@a-cube-io/ereceipts-js-sdk 2.0.9 → 2.1.1
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 +273 -3459
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +81 -1502
- package/dist/index.esm.js +295 -3403
- package/dist/index.esm.js.map +1 -1
- package/dist/index.native.js +295 -3403
- package/dist/index.native.js.map +1 -1
- package/package.json +1 -1
package/dist/index.native.js
CHANGED
|
@@ -1869,27 +1869,6 @@ function clearObject(input) {
|
|
|
1869
1869
|
}
|
|
1870
1870
|
return input;
|
|
1871
1871
|
}
|
|
1872
|
-
function clearObjectShallow(obj) {
|
|
1873
|
-
if (!obj || typeof obj !== 'object') {
|
|
1874
|
-
return {};
|
|
1875
|
-
}
|
|
1876
|
-
const cleaned = {};
|
|
1877
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
1878
|
-
if (value !== null && value !== undefined && value !== '') {
|
|
1879
|
-
cleaned[key] = value;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
return cleaned;
|
|
1883
|
-
}
|
|
1884
|
-
function isEmpty(value) {
|
|
1885
|
-
return value === null || value === undefined || value === '';
|
|
1886
|
-
}
|
|
1887
|
-
function hasNonEmptyValues(obj) {
|
|
1888
|
-
if (!obj || typeof obj !== 'object') {
|
|
1889
|
-
return false;
|
|
1890
|
-
}
|
|
1891
|
-
return Object.values(obj).some((value) => !isEmpty(value));
|
|
1892
|
-
}
|
|
1893
1872
|
|
|
1894
1873
|
/**
|
|
1895
1874
|
* Platform detection utilities
|
|
@@ -2112,7 +2091,7 @@ function formatDecimal(value, decimals = 2) {
|
|
|
2112
2091
|
return num.toFixed(decimals);
|
|
2113
2092
|
}
|
|
2114
2093
|
|
|
2115
|
-
const log$
|
|
2094
|
+
const log$b = createPrefixedLogger('AUTH-SERVICE');
|
|
2116
2095
|
class AuthenticationService {
|
|
2117
2096
|
get user$() {
|
|
2118
2097
|
return this.userSubject.asObservable();
|
|
@@ -2134,7 +2113,7 @@ class AuthenticationService {
|
|
|
2134
2113
|
}
|
|
2135
2114
|
async login(credentials) {
|
|
2136
2115
|
this.authStateSubject.next('authenticating');
|
|
2137
|
-
log$
|
|
2116
|
+
log$b.info('Login attempt', {
|
|
2138
2117
|
authUrl: this.config.authUrl,
|
|
2139
2118
|
email: credentials.email,
|
|
2140
2119
|
});
|
|
@@ -2145,7 +2124,7 @@ class AuthenticationService {
|
|
|
2145
2124
|
});
|
|
2146
2125
|
const jwtPayload = parseJwt(response.data.token);
|
|
2147
2126
|
const expiresAt = jwtPayload.exp * 1000;
|
|
2148
|
-
log$
|
|
2127
|
+
log$b.info('Login successful', {
|
|
2149
2128
|
authUrl: this.config.authUrl,
|
|
2150
2129
|
tokenPrefix: response.data.token.substring(0, 30) + '...',
|
|
2151
2130
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
@@ -2174,21 +2153,21 @@ class AuthenticationService {
|
|
|
2174
2153
|
const token = await this.tokenStorage.getAccessToken();
|
|
2175
2154
|
if (!token) {
|
|
2176
2155
|
// No token - clear any stale user state
|
|
2177
|
-
log$
|
|
2156
|
+
log$b.debug('getCurrentUser: No token in storage');
|
|
2178
2157
|
if (this.userSubject.value) {
|
|
2179
2158
|
this.userSubject.next(null);
|
|
2180
2159
|
this.authStateSubject.next('idle');
|
|
2181
2160
|
}
|
|
2182
2161
|
return null;
|
|
2183
2162
|
}
|
|
2184
|
-
log$
|
|
2163
|
+
log$b.debug('getCurrentUser: Token found', {
|
|
2185
2164
|
tokenPrefix: token.substring(0, 30) + '...',
|
|
2186
2165
|
tokenLength: token.length,
|
|
2187
2166
|
});
|
|
2188
2167
|
const jwtPayload = parseJwt(token);
|
|
2189
2168
|
if (isTokenExpired(jwtPayload)) {
|
|
2190
2169
|
// Token expired - clear everything
|
|
2191
|
-
log$
|
|
2170
|
+
log$b.warn('getCurrentUser: Token expired');
|
|
2192
2171
|
await this.tokenStorage.clearTokens();
|
|
2193
2172
|
this.userSubject.next(null);
|
|
2194
2173
|
this.authStateSubject.next('idle');
|
|
@@ -2198,7 +2177,7 @@ class AuthenticationService {
|
|
|
2198
2177
|
// Token is valid - return cached user if available
|
|
2199
2178
|
const currentUser = this.userSubject.value;
|
|
2200
2179
|
if (currentUser) {
|
|
2201
|
-
log$
|
|
2180
|
+
log$b.debug('getCurrentUser: Returning cached user', {
|
|
2202
2181
|
email: currentUser.email,
|
|
2203
2182
|
roles: currentUser.roles,
|
|
2204
2183
|
});
|
|
@@ -2221,12 +2200,12 @@ class AuthenticationService {
|
|
|
2221
2200
|
async isAuthenticated() {
|
|
2222
2201
|
const token = await this.tokenStorage.getAccessToken();
|
|
2223
2202
|
if (!token) {
|
|
2224
|
-
log$
|
|
2203
|
+
log$b.debug('isAuthenticated: No token in storage');
|
|
2225
2204
|
return false;
|
|
2226
2205
|
}
|
|
2227
2206
|
const jwtPayload = parseJwt(token);
|
|
2228
2207
|
const expired = isTokenExpired(jwtPayload);
|
|
2229
|
-
log$
|
|
2208
|
+
log$b.debug('isAuthenticated: Token check', {
|
|
2230
2209
|
hasToken: true,
|
|
2231
2210
|
expired,
|
|
2232
2211
|
expiresAt: new Date(jwtPayload.exp * 1000).toISOString(),
|
|
@@ -2446,7 +2425,7 @@ class ConfigManager {
|
|
|
2446
2425
|
}
|
|
2447
2426
|
}
|
|
2448
2427
|
|
|
2449
|
-
// Enum options arrays
|
|
2428
|
+
// Enum options arrays (used by consumers for dropdowns and form validation)
|
|
2450
2429
|
const VAT_RATE_CODE_OPTIONS = [
|
|
2451
2430
|
'4.00',
|
|
2452
2431
|
'5.00',
|
|
@@ -2473,103 +2452,10 @@ const VAT_RATE_CODE_OPTIONS = [
|
|
|
2473
2452
|
];
|
|
2474
2453
|
const GOOD_OR_SERVICE_OPTIONS = ['goods', 'service'];
|
|
2475
2454
|
const RECEIPT_PROOF_TYPE_OPTIONS = ['POS', 'VR', 'ND'];
|
|
2476
|
-
// Enum
|
|
2455
|
+
// Enum schemas (used by consumers as building blocks for their own form schemas)
|
|
2477
2456
|
const VatRateCodeSchema = z.enum(VAT_RATE_CODE_OPTIONS);
|
|
2478
2457
|
const GoodOrServiceSchema = z.enum(GOOD_OR_SERVICE_OPTIONS);
|
|
2479
2458
|
const ReceiptProofTypeSchema = z.enum(RECEIPT_PROOF_TYPE_OPTIONS);
|
|
2480
|
-
// Receipt Item Schema
|
|
2481
|
-
const ReceiptItemSchema = z.object({
|
|
2482
|
-
type: GoodOrServiceSchema.optional(),
|
|
2483
|
-
quantity: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2484
|
-
description: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2485
|
-
unit_price: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2486
|
-
vat_rate_code: VatRateCodeSchema.optional(),
|
|
2487
|
-
simplified_vat_allocation: z.boolean().optional(),
|
|
2488
|
-
discount: z.string().nullable().optional(),
|
|
2489
|
-
is_down_payment_or_voucher_redemption: z.boolean().optional(),
|
|
2490
|
-
complimentary: z.boolean().optional(),
|
|
2491
|
-
});
|
|
2492
|
-
// Main Receipt Input Schema
|
|
2493
|
-
const ReceiptInputSchema = z
|
|
2494
|
-
.object({
|
|
2495
|
-
items: z.array(ReceiptItemSchema).min(1, { error: 'arrayMin1' }),
|
|
2496
|
-
customer_tax_code: z.string().optional(),
|
|
2497
|
-
customer_lottery_code: z.string().optional(),
|
|
2498
|
-
discount: z.string().nullable().optional(),
|
|
2499
|
-
invoice_issuing: z.boolean().optional(),
|
|
2500
|
-
uncollected_dcr_to_ssn: z.boolean().optional(),
|
|
2501
|
-
services_uncollected_amount: z.string().nullable().optional(),
|
|
2502
|
-
goods_uncollected_amount: z.string().nullable().optional(),
|
|
2503
|
-
cash_payment_amount: z.string().nullable().optional(),
|
|
2504
|
-
electronic_payment_amount: z.string().nullable().optional(),
|
|
2505
|
-
ticket_restaurant_payment_amount: z.string().nullable().optional(),
|
|
2506
|
-
ticket_restaurant_quantity: z.number().optional(),
|
|
2507
|
-
})
|
|
2508
|
-
.refine((data) => {
|
|
2509
|
-
// At least one payment method should be provided
|
|
2510
|
-
const hasCashPayment = data.cash_payment_amount && parseFloat(data.cash_payment_amount) > 0;
|
|
2511
|
-
const hasElectronicPayment = data.electronic_payment_amount && parseFloat(data.electronic_payment_amount) > 0;
|
|
2512
|
-
const hasTicketPayment = data.ticket_restaurant_payment_amount &&
|
|
2513
|
-
parseFloat(data.ticket_restaurant_payment_amount) > 0;
|
|
2514
|
-
return hasCashPayment || hasElectronicPayment || hasTicketPayment;
|
|
2515
|
-
}, {
|
|
2516
|
-
error: 'At least one payment method is required',
|
|
2517
|
-
path: ['payment_methods'],
|
|
2518
|
-
})
|
|
2519
|
-
.refine((data) => {
|
|
2520
|
-
// only one between customer_tax_code and customer_lottery_code can be provided
|
|
2521
|
-
return !data.customer_tax_code || !data.customer_lottery_code;
|
|
2522
|
-
}, {
|
|
2523
|
-
error: 'Only one between customer_tax_code and customer_lottery_code can be provided',
|
|
2524
|
-
path: ['customer_tax_code', 'customer_lottery_code'],
|
|
2525
|
-
});
|
|
2526
|
-
// Receipt Return or Void via PEM Schema
|
|
2527
|
-
const ReceiptReturnOrVoidViaPEMInputSchema = z.object({
|
|
2528
|
-
device_id: z.string().optional(),
|
|
2529
|
-
items: z.array(ReceiptItemSchema).min(1, { error: 'arrayMin1' }),
|
|
2530
|
-
document_number: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2531
|
-
document_datetime: z.string().optional(),
|
|
2532
|
-
lottery_code: z.string().optional(),
|
|
2533
|
-
});
|
|
2534
|
-
// Receipt Return or Void with Proof Schema
|
|
2535
|
-
const ReceiptReturnOrVoidWithProofInputSchema = z.object({
|
|
2536
|
-
items: z.array(ReceiptItemSchema).min(1, { error: 'arrayMin1' }),
|
|
2537
|
-
proof: ReceiptProofTypeSchema,
|
|
2538
|
-
document_datetime: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2539
|
-
});
|
|
2540
|
-
// Void Receipt Schema
|
|
2541
|
-
const VoidReceiptInputSchema = z.object({
|
|
2542
|
-
document_number: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2543
|
-
});
|
|
2544
|
-
const ReceiptReturnItemSchema = z
|
|
2545
|
-
.array(z.object({
|
|
2546
|
-
id: z.number(),
|
|
2547
|
-
quantity: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2548
|
-
}))
|
|
2549
|
-
.min(1, { error: 'arrayMin1' });
|
|
2550
|
-
// Receipt Return Schema
|
|
2551
|
-
const ReceiptReturnInputSchema = z.object({
|
|
2552
|
-
items: z.array(ReceiptReturnItemSchema).min(1, { error: 'arrayMin1' }),
|
|
2553
|
-
document_number: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2554
|
-
});
|
|
2555
|
-
|
|
2556
|
-
// Cashier Create Input Schema (MF1)
|
|
2557
|
-
const CashierCreateInputSchema = z.object({
|
|
2558
|
-
email: z
|
|
2559
|
-
.string()
|
|
2560
|
-
.min(1, { error: 'fieldIsRequired' })
|
|
2561
|
-
.max(255, { error: 'emailMaxLength' })
|
|
2562
|
-
.email({ error: 'invalidEmail' }),
|
|
2563
|
-
password: z
|
|
2564
|
-
.string()
|
|
2565
|
-
.min(8, { error: 'passwordMinLength' })
|
|
2566
|
-
.max(40, { error: 'passwordMaxLength' }),
|
|
2567
|
-
name: z.string().min(1, { error: 'fieldIsRequired' }).max(255, { error: 'nameMaxLength' }),
|
|
2568
|
-
display_name: z
|
|
2569
|
-
.string()
|
|
2570
|
-
.min(1, { error: 'fieldIsRequired' })
|
|
2571
|
-
.max(255, { error: 'displayNameMaxLength' }),
|
|
2572
|
-
});
|
|
2573
2459
|
|
|
2574
2460
|
// Enum options arrays
|
|
2575
2461
|
const PEM_STATUS_OPTIONS = [
|
|
@@ -2580,312 +2466,12 @@ const PEM_STATUS_OPTIONS = [
|
|
|
2580
2466
|
'OFFLINE',
|
|
2581
2467
|
'DISCARDED',
|
|
2582
2468
|
];
|
|
2583
|
-
// Address Schema (reusable)
|
|
2584
|
-
const AddressSchema = z.object({
|
|
2585
|
-
street_address: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2586
|
-
street_number: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2587
|
-
zip_code: z
|
|
2588
|
-
.string()
|
|
2589
|
-
.min(1, { error: 'fieldIsRequired' })
|
|
2590
|
-
.regex(/^\d{5}$/, { error: 'invalidZipCode' }),
|
|
2591
|
-
city: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2592
|
-
province: z
|
|
2593
|
-
.string()
|
|
2594
|
-
.min(2, { error: 'provinceMinLength' })
|
|
2595
|
-
.max(2, { error: 'provinceMaxLength' })
|
|
2596
|
-
.toUpperCase(),
|
|
2597
|
-
});
|
|
2598
|
-
// PEM Status Schema
|
|
2599
|
-
const PEMStatusSchema = z.enum(PEM_STATUS_OPTIONS);
|
|
2600
|
-
// Activation Request Schema
|
|
2601
|
-
const ActivationRequestSchema = z.object({
|
|
2602
|
-
registration_key: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2603
|
-
});
|
|
2604
|
-
// PEM Status Offline Request Schema
|
|
2605
|
-
const PEMStatusOfflineRequestSchema = z.object({
|
|
2606
|
-
timestamp: z
|
|
2607
|
-
.string()
|
|
2608
|
-
.min(1, { error: 'fieldIsRequired' })
|
|
2609
|
-
.refine((val) => !isNaN(Date.parse(val)), {
|
|
2610
|
-
error: 'invalidDateFormat',
|
|
2611
|
-
}),
|
|
2612
|
-
reason: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2613
|
-
});
|
|
2614
|
-
|
|
2615
|
-
// Cash Register Create Schema
|
|
2616
|
-
const CashRegisterCreateSchema = z.object({
|
|
2617
|
-
pem_serial_number: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2618
|
-
name: z.string().min(1, { error: 'fieldIsRequired' }).max(100, { error: 'nameMaxLength' }),
|
|
2619
|
-
});
|
|
2620
|
-
|
|
2621
|
-
// VAT number validation regex (Partita IVA - 11 digits)
|
|
2622
|
-
const VAT_NUMBER_REGEX = /^\d{11}$/;
|
|
2623
|
-
// Fiscal code validation regex (Codice Fiscale - 11 digits only for merchants)
|
|
2624
|
-
const FISCAL_CODE_REGEX = /^\d{11}$/;
|
|
2625
|
-
// Password validation regex (from OpenAPI spec)
|
|
2626
|
-
const PASSWORD_REGEX = /^((?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{10,}).*)$/;
|
|
2627
|
-
// Merchant Create Input Schema
|
|
2628
|
-
const MerchantCreateInputSchema = z
|
|
2629
|
-
.object({
|
|
2630
|
-
vat_number: z
|
|
2631
|
-
.string()
|
|
2632
|
-
.min(1, { error: 'fieldIsRequired' })
|
|
2633
|
-
.regex(VAT_NUMBER_REGEX, { error: 'invalidVatNumber' }),
|
|
2634
|
-
fiscal_code: z.string().regex(FISCAL_CODE_REGEX, { error: 'invalidFiscalCode' }).optional(),
|
|
2635
|
-
business_name: z.string().max(200, { error: 'businessNameMaxLength' }).optional().nullable(),
|
|
2636
|
-
first_name: z.string().max(100, { error: 'firstNameMaxLength' }).optional().nullable(),
|
|
2637
|
-
last_name: z.string().max(100, { error: 'lastNameMaxLength' }).optional().nullable(),
|
|
2638
|
-
email: z.string().min(1, { error: 'fieldIsRequired' }).email({ error: 'invalidEmail' }),
|
|
2639
|
-
password: z
|
|
2640
|
-
.string()
|
|
2641
|
-
.min(1, { error: 'fieldIsRequired' })
|
|
2642
|
-
.regex(PASSWORD_REGEX, { error: 'passwordComplexity' }),
|
|
2643
|
-
address: AddressSchema.optional(),
|
|
2644
|
-
})
|
|
2645
|
-
.refine((data) => {
|
|
2646
|
-
const hasBusinessName = data.business_name && data.business_name.trim() !== '';
|
|
2647
|
-
const hasPersonalNames = (data.first_name && data.first_name.trim() !== '') ||
|
|
2648
|
-
(data.last_name && data.last_name.trim() !== '');
|
|
2649
|
-
// If business name is set, first/last name must not be provided
|
|
2650
|
-
if (hasBusinessName && hasPersonalNames) {
|
|
2651
|
-
return false;
|
|
2652
|
-
}
|
|
2653
|
-
// At least one naming method must be provided
|
|
2654
|
-
if (!hasBusinessName && !hasPersonalNames) {
|
|
2655
|
-
return false;
|
|
2656
|
-
}
|
|
2657
|
-
return true;
|
|
2658
|
-
}, {
|
|
2659
|
-
error: 'businessNameOrPersonalNamesRequired',
|
|
2660
|
-
path: ['business_name'],
|
|
2661
|
-
});
|
|
2662
|
-
// Merchant Update Input Schema
|
|
2663
|
-
const MerchantUpdateInputSchema = z.object({
|
|
2664
|
-
business_name: z.string().max(200, { error: 'businessNameMaxLength' }).optional().nullable(),
|
|
2665
|
-
first_name: z.string().max(100, { error: 'firstNameMaxLength' }).optional().nullable(),
|
|
2666
|
-
last_name: z.string().max(100, { error: 'lastNameMaxLength' }).optional().nullable(),
|
|
2667
|
-
address: AddressSchema.optional().nullable(),
|
|
2668
|
-
});
|
|
2669
2469
|
|
|
2670
2470
|
// Enum options arrays
|
|
2671
2471
|
const PEM_TYPE_OPTIONS = ['AP', 'SP', 'TM', 'PV'];
|
|
2672
|
-
// PEM Data Schema
|
|
2673
|
-
const PemDataSchema = z.object({
|
|
2674
|
-
version: z.string().min(1, { error: 'fieldIsRequired' }),
|
|
2675
|
-
type: z.enum(PEM_TYPE_OPTIONS, {
|
|
2676
|
-
error: 'invalidPemType',
|
|
2677
|
-
}),
|
|
2678
|
-
});
|
|
2679
|
-
// PEM Create Input Schema
|
|
2680
|
-
const PemCreateInputSchema = z.object({
|
|
2681
|
-
merchant_uuid: z.string().min(1, { error: 'fieldIsRequired' }).uuid({ error: 'invalidUuid' }),
|
|
2682
|
-
address: AddressSchema.optional(),
|
|
2683
|
-
/* external_pem_data: PemDataSchema.optional(), */
|
|
2684
|
-
});
|
|
2685
2472
|
|
|
2686
|
-
//
|
|
2687
|
-
const FISCAL_ID_REGEX = /^([A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]|[0-9]{11})$/;
|
|
2688
|
-
// Supplier Create Input Schema
|
|
2689
|
-
const SupplierCreateInputSchema = z.object({
|
|
2690
|
-
fiscal_id: z
|
|
2691
|
-
.string()
|
|
2692
|
-
.min(1, { error: 'fieldIsRequired' })
|
|
2693
|
-
.regex(FISCAL_ID_REGEX, { error: 'invalidFiscalId' })
|
|
2694
|
-
.toUpperCase(),
|
|
2695
|
-
name: z.string().min(1, { error: 'fieldIsRequired' }).max(200, { error: 'nameMaxLength' }),
|
|
2696
|
-
address: AddressSchema.optional(),
|
|
2697
|
-
});
|
|
2698
|
-
// Supplier Update Input Schema
|
|
2699
|
-
const SupplierUpdateInputSchema = z.object({
|
|
2700
|
-
name: z.string().min(1, { error: 'fieldIsRequired' }).max(200, { error: 'nameMaxLength' }),
|
|
2701
|
-
address: AddressSchema.optional(),
|
|
2702
|
-
});
|
|
2703
|
-
|
|
2704
|
-
// Journal Close Input Schema
|
|
2705
|
-
const JournalCloseInputSchema = z.object({
|
|
2706
|
-
closing_timestamp: z
|
|
2707
|
-
.string()
|
|
2708
|
-
.min(1, { error: 'fieldIsRequired' })
|
|
2709
|
-
.refine((val) => !isNaN(Date.parse(val)), {
|
|
2710
|
-
error: 'invalidDateFormat',
|
|
2711
|
-
}),
|
|
2712
|
-
reason: z.string().max(255, { error: 'reasonMaxLength' }).optional(),
|
|
2713
|
-
});
|
|
2714
|
-
|
|
2715
|
-
// Daily Report Status Options
|
|
2473
|
+
// Enum options arrays
|
|
2716
2474
|
const DAILY_REPORT_STATUS_OPTIONS = ['pending', 'sent', 'error'];
|
|
2717
|
-
// Daily Report Status Schema
|
|
2718
|
-
const DailyReportStatusSchema = z.enum(DAILY_REPORT_STATUS_OPTIONS, {
|
|
2719
|
-
error: 'invalidDailyReportStatus',
|
|
2720
|
-
});
|
|
2721
|
-
// Daily Reports List Parameters Schema
|
|
2722
|
-
const DailyReportsParamsSchema = z.object({
|
|
2723
|
-
pem_serial_number: z.string().min(1, { error: 'fieldIsRequired' }).optional(),
|
|
2724
|
-
date_from: z
|
|
2725
|
-
.string()
|
|
2726
|
-
.refine((val) => !isNaN(Date.parse(val)), {
|
|
2727
|
-
error: 'invalidDateFormat',
|
|
2728
|
-
})
|
|
2729
|
-
.optional(),
|
|
2730
|
-
date_to: z
|
|
2731
|
-
.string()
|
|
2732
|
-
.refine((val) => !isNaN(Date.parse(val)), {
|
|
2733
|
-
error: 'invalidDateFormat',
|
|
2734
|
-
})
|
|
2735
|
-
.optional(),
|
|
2736
|
-
status: DailyReportStatusSchema.optional(),
|
|
2737
|
-
page: z.number().min(1, { error: 'pageMinValue' }).optional(),
|
|
2738
|
-
});
|
|
2739
|
-
|
|
2740
|
-
const NotificationScopeSchema = z.object({
|
|
2741
|
-
type: z.literal('global'),
|
|
2742
|
-
});
|
|
2743
|
-
const PemStatusSchema = z.enum(['ONLINE', 'OFFLINE']);
|
|
2744
|
-
const NotificationDataBlockAtSchema = z.object({
|
|
2745
|
-
block_at: z.string(),
|
|
2746
|
-
});
|
|
2747
|
-
const NotificationDataPemStatusSchema = z.object({
|
|
2748
|
-
from: PemStatusSchema,
|
|
2749
|
-
to: PemStatusSchema,
|
|
2750
|
-
});
|
|
2751
|
-
const NotificationBaseSchema = z.object({
|
|
2752
|
-
uuid: z.string().uuid({ error: 'invalidUuid' }),
|
|
2753
|
-
scope: NotificationScopeSchema,
|
|
2754
|
-
source: z.enum(['system', 'Italian Tax Authority']),
|
|
2755
|
-
level: z.enum(['info', 'warning', 'error', 'critical']),
|
|
2756
|
-
created_at: z.string(),
|
|
2757
|
-
});
|
|
2758
|
-
const NotificationMf2UnreachableSchema = NotificationBaseSchema.extend({
|
|
2759
|
-
type: z.literal('INTERNAL_COMMUNICATION_FAILURE'),
|
|
2760
|
-
code: z.literal('SYS-W-01'),
|
|
2761
|
-
data: NotificationDataBlockAtSchema,
|
|
2762
|
-
});
|
|
2763
|
-
const NotificationPemsBlockedSchema = NotificationBaseSchema.extend({
|
|
2764
|
-
type: z.literal('PEM_STATUS_CHANGED'),
|
|
2765
|
-
code: z.literal('SYS-C-01'),
|
|
2766
|
-
data: NotificationDataPemStatusSchema,
|
|
2767
|
-
});
|
|
2768
|
-
const NotificationPemBackOnlineSchema = NotificationBaseSchema.extend({
|
|
2769
|
-
type: z.literal('PEM_STATUS_CHANGED'),
|
|
2770
|
-
code: z.literal('SYS-I-01'),
|
|
2771
|
-
data: NotificationDataPemStatusSchema,
|
|
2772
|
-
});
|
|
2773
|
-
const NotificationSchema = z.discriminatedUnion('code', [
|
|
2774
|
-
NotificationMf2UnreachableSchema,
|
|
2775
|
-
NotificationPemsBlockedSchema,
|
|
2776
|
-
NotificationPemBackOnlineSchema,
|
|
2777
|
-
]);
|
|
2778
|
-
const NotificationListResponseSchema = z.object({
|
|
2779
|
-
members: z.array(NotificationSchema),
|
|
2780
|
-
});
|
|
2781
|
-
|
|
2782
|
-
const TelemetryMerchantSchema = z.object({
|
|
2783
|
-
vat_number: z.string(),
|
|
2784
|
-
fiscal_code: z.string().nullable(),
|
|
2785
|
-
business_name: z.string(),
|
|
2786
|
-
});
|
|
2787
|
-
const TelemetrySupplierSchema = z.object({
|
|
2788
|
-
vat_number: z.string(),
|
|
2789
|
-
fiscal_code: z.string().nullable(),
|
|
2790
|
-
business_name: z.string(),
|
|
2791
|
-
});
|
|
2792
|
-
const TelemetrySoftwareVersionSchema = z.object({
|
|
2793
|
-
version: z.string(),
|
|
2794
|
-
swid: z.string(),
|
|
2795
|
-
installed_at: z.string(),
|
|
2796
|
-
status: z.enum(['active', 'inactive', 'archived']),
|
|
2797
|
-
});
|
|
2798
|
-
const TelemetrySoftwareSchema = z.object({
|
|
2799
|
-
code: z.string(),
|
|
2800
|
-
name: z.string(),
|
|
2801
|
-
approval_reference: z.string(),
|
|
2802
|
-
version_info: TelemetrySoftwareVersionSchema,
|
|
2803
|
-
});
|
|
2804
|
-
const PendingReceiptsSchema = z.object({
|
|
2805
|
-
count: z.number().int().nonnegative(),
|
|
2806
|
-
total_amount: z.string(),
|
|
2807
|
-
});
|
|
2808
|
-
const TransmissionSchema = z.object({
|
|
2809
|
-
attempted_at: z.string(),
|
|
2810
|
-
outcome: z.enum(['success', 'failed', 'pending']),
|
|
2811
|
-
});
|
|
2812
|
-
const MessageSchema = z.object({
|
|
2813
|
-
received_at: z.string(),
|
|
2814
|
-
content: z.string(),
|
|
2815
|
-
});
|
|
2816
|
-
const LotterySecretRequestSchema = z.object({
|
|
2817
|
-
requested_at: z.string(),
|
|
2818
|
-
outcome: z.enum(['success', 'failed', 'pending']),
|
|
2819
|
-
});
|
|
2820
|
-
const LotterySchema = z.object({
|
|
2821
|
-
last_transmission: TransmissionSchema,
|
|
2822
|
-
secret_request: LotterySecretRequestSchema,
|
|
2823
|
-
});
|
|
2824
|
-
const TelemetrySchema = z.object({
|
|
2825
|
-
pem_id: z.string(),
|
|
2826
|
-
pem_status: z.enum(['ONLINE', 'OFFLINE', 'ERROR']),
|
|
2827
|
-
pem_status_changed_at: z.string(),
|
|
2828
|
-
merchant: TelemetryMerchantSchema,
|
|
2829
|
-
supplier: TelemetrySupplierSchema,
|
|
2830
|
-
software: TelemetrySoftwareSchema,
|
|
2831
|
-
last_communication_at: z.string(),
|
|
2832
|
-
pending_receipts: PendingReceiptsSchema,
|
|
2833
|
-
last_receipt_transmission: TransmissionSchema,
|
|
2834
|
-
last_message_from_mf2: MessageSchema,
|
|
2835
|
-
ade_corrispettivi_transmission: TransmissionSchema,
|
|
2836
|
-
last_message_from_ade: MessageSchema,
|
|
2837
|
-
lottery: LotterySchema,
|
|
2838
|
-
});
|
|
2839
|
-
|
|
2840
|
-
// Receipt schemas and types
|
|
2841
|
-
// Common validation utilities
|
|
2842
|
-
const ValidationMessages = {
|
|
2843
|
-
fieldIsRequired: 'This field is required',
|
|
2844
|
-
arrayMin1: 'At least one item is required',
|
|
2845
|
-
paymentMethodRequired: 'At least one payment method is required',
|
|
2846
|
-
invalidEmail: 'Please enter a valid email address',
|
|
2847
|
-
passwordMinLength: 'Password must be at least 8 characters long',
|
|
2848
|
-
passwordComplexity: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character',
|
|
2849
|
-
invalidZipCode: 'Please enter a valid 5-digit zip code',
|
|
2850
|
-
provinceMinLength: 'Province code must be 2 characters',
|
|
2851
|
-
provinceMaxLength: 'Province code must be 2 characters',
|
|
2852
|
-
invalidDateFormat: 'Please enter a valid date',
|
|
2853
|
-
nameMaxLength: 'Name is too long',
|
|
2854
|
-
invalidFiscalId: 'Please enter a valid Italian fiscal ID (Codice Fiscale or Partita IVA)',
|
|
2855
|
-
invalidVatNumber: 'Please enter a valid VAT number (11 digits)',
|
|
2856
|
-
invalidFiscalCode: 'Please enter a valid fiscal code (11 digits)',
|
|
2857
|
-
businessNameMaxLength: 'Business name is too long (max 200 characters)',
|
|
2858
|
-
businessNameOrPersonalNamesRequired: 'Please provide either a business name or first/last name, but not both',
|
|
2859
|
-
firstNameMaxLength: 'First name is too long (max 100 characters)',
|
|
2860
|
-
lastNameMaxLength: 'Last name is too long (max 100 characters)',
|
|
2861
|
-
invalidUuid: 'Please enter a valid UUID',
|
|
2862
|
-
invalidPemType: 'PEM type must be one of: AP, SP, TM, PV',
|
|
2863
|
-
reasonMaxLength: 'Reason is too long (max 255 characters)',
|
|
2864
|
-
pageMinValue: 'Page number must be at least 1',
|
|
2865
|
-
invalidDailyReportStatus: 'Daily report status must be one of: pending, sent, error',
|
|
2866
|
-
displayNameMaxLength: 'Display name is too long (max 255 characters)',
|
|
2867
|
-
};
|
|
2868
|
-
// Validation helper functions
|
|
2869
|
-
const validateInput = (schema, data) => {
|
|
2870
|
-
const result = schema.safeParse(data);
|
|
2871
|
-
if (!result.success) {
|
|
2872
|
-
const errors = result.error.issues.map((error) => ({
|
|
2873
|
-
field: error.path.join('.'),
|
|
2874
|
-
message: error.message,
|
|
2875
|
-
code: error.code,
|
|
2876
|
-
}));
|
|
2877
|
-
return {
|
|
2878
|
-
success: false,
|
|
2879
|
-
errors,
|
|
2880
|
-
data: null,
|
|
2881
|
-
};
|
|
2882
|
-
}
|
|
2883
|
-
return {
|
|
2884
|
-
success: true,
|
|
2885
|
-
errors: [],
|
|
2886
|
-
data: result.data,
|
|
2887
|
-
};
|
|
2888
|
-
};
|
|
2889
2475
|
|
|
2890
2476
|
class ACubeSDKError extends Error {
|
|
2891
2477
|
constructor(type, message, originalError, statusCode, violations) {
|
|
@@ -2898,7 +2484,7 @@ class ACubeSDKError extends Error {
|
|
|
2898
2484
|
}
|
|
2899
2485
|
}
|
|
2900
2486
|
|
|
2901
|
-
const log$
|
|
2487
|
+
const log$a = createPrefixedLogger('AUTH-STRATEGY');
|
|
2902
2488
|
class AuthStrategy {
|
|
2903
2489
|
constructor(jwtHandler, mtlsHandler, userProvider, mtlsAdapter) {
|
|
2904
2490
|
this.jwtHandler = jwtHandler;
|
|
@@ -2913,7 +2499,7 @@ class AuthStrategy {
|
|
|
2913
2499
|
const platform = this.detectPlatform();
|
|
2914
2500
|
const userRole = await this.getUserRole();
|
|
2915
2501
|
const isReceiptEndpoint = this.isReceiptEndpoint(url);
|
|
2916
|
-
log$
|
|
2502
|
+
log$a.debug('Determining auth config', {
|
|
2917
2503
|
url,
|
|
2918
2504
|
method,
|
|
2919
2505
|
platform,
|
|
@@ -3044,7 +2630,7 @@ class JwtAuthHandler {
|
|
|
3044
2630
|
}
|
|
3045
2631
|
}
|
|
3046
2632
|
|
|
3047
|
-
const log$
|
|
2633
|
+
const log$9 = createPrefixedLogger('MTLS-HANDLER');
|
|
3048
2634
|
class MtlsAuthHandler {
|
|
3049
2635
|
constructor(mtlsAdapter, certificatePort) {
|
|
3050
2636
|
this.mtlsAdapter = mtlsAdapter;
|
|
@@ -3086,7 +2672,7 @@ class MtlsAuthHandler {
|
|
|
3086
2672
|
async makeRequest(url, config, jwtToken) {
|
|
3087
2673
|
const requestKey = this.generateRequestKey(url, config, jwtToken);
|
|
3088
2674
|
if (this.pendingRequests.has(requestKey)) {
|
|
3089
|
-
log$
|
|
2675
|
+
log$9.debug('Deduplicating concurrent request:', url);
|
|
3090
2676
|
return this.pendingRequests.get(requestKey);
|
|
3091
2677
|
}
|
|
3092
2678
|
const requestPromise = this.executeRequest(url, config, jwtToken, false);
|
|
@@ -3110,10 +2696,10 @@ class MtlsAuthHandler {
|
|
|
3110
2696
|
};
|
|
3111
2697
|
if (jwtToken) {
|
|
3112
2698
|
headers['Authorization'] = `Bearer ${jwtToken}`;
|
|
3113
|
-
log$
|
|
2699
|
+
log$9.debug('JWT token present:', jwtToken.substring(0, 20) + '...');
|
|
3114
2700
|
}
|
|
3115
2701
|
else {
|
|
3116
|
-
log$
|
|
2702
|
+
log$9.warn('No JWT token provided for mTLS request');
|
|
3117
2703
|
}
|
|
3118
2704
|
const fullUrl = this.constructMtlsUrl(url);
|
|
3119
2705
|
const mtlsConfig = {
|
|
@@ -3124,25 +2710,25 @@ class MtlsAuthHandler {
|
|
|
3124
2710
|
timeout: config.timeout,
|
|
3125
2711
|
responseType: config.responseType,
|
|
3126
2712
|
};
|
|
3127
|
-
log$
|
|
3128
|
-
log$
|
|
2713
|
+
log$9.debug('header-mtls', headers);
|
|
2714
|
+
log$9.debug(`${config.method} ${fullUrl}`);
|
|
3129
2715
|
if (config.data) {
|
|
3130
|
-
log$
|
|
2716
|
+
log$9.debug('Request body:', config.data);
|
|
3131
2717
|
}
|
|
3132
2718
|
try {
|
|
3133
2719
|
const response = await this.mtlsAdapter.request(mtlsConfig);
|
|
3134
|
-
log$
|
|
2720
|
+
log$9.debug(`Response ${response.status} from ${fullUrl}`);
|
|
3135
2721
|
if (response.data) {
|
|
3136
|
-
log$
|
|
2722
|
+
log$9.debug('Response body:', response.data);
|
|
3137
2723
|
}
|
|
3138
2724
|
return response.data;
|
|
3139
2725
|
}
|
|
3140
2726
|
catch (error) {
|
|
3141
|
-
log$
|
|
2727
|
+
log$9.error(`Response error from ${fullUrl}:`, error);
|
|
3142
2728
|
if (error && typeof error === 'object' && 'response' in error) {
|
|
3143
2729
|
const axiosError = error;
|
|
3144
2730
|
if (axiosError.response?.data) {
|
|
3145
|
-
log$
|
|
2731
|
+
log$9.error('Response body:', axiosError.response.data);
|
|
3146
2732
|
}
|
|
3147
2733
|
}
|
|
3148
2734
|
if (isRetryAttempt) {
|
|
@@ -3152,7 +2738,7 @@ class MtlsAuthHandler {
|
|
|
3152
2738
|
if (!shouldRetry) {
|
|
3153
2739
|
throw error;
|
|
3154
2740
|
}
|
|
3155
|
-
log$
|
|
2741
|
+
log$9.debug('Request failed, reconfiguring certificate and retrying...');
|
|
3156
2742
|
try {
|
|
3157
2743
|
await this.configureCertificate(certificate);
|
|
3158
2744
|
return await this.executeRequest(url, config, jwtToken, true);
|
|
@@ -3167,2000 +2753,99 @@ class MtlsAuthHandler {
|
|
|
3167
2753
|
const message = error.message.toLowerCase();
|
|
3168
2754
|
return (message.includes('certificate') ||
|
|
3169
2755
|
message.includes('ssl') ||
|
|
3170
|
-
message.includes('tls') ||
|
|
3171
|
-
message.includes('handshake'));
|
|
3172
|
-
}
|
|
3173
|
-
return false;
|
|
3174
|
-
}
|
|
3175
|
-
async configureCertificate(certificate) {
|
|
3176
|
-
if (!this.mtlsAdapter) {
|
|
3177
|
-
throw new Error('mTLS adapter not available');
|
|
3178
|
-
}
|
|
3179
|
-
const certificateData = {
|
|
3180
|
-
certificate: certificate.certificate,
|
|
3181
|
-
privateKey: certificate.privateKey,
|
|
3182
|
-
format: certificate.format.toUpperCase(),
|
|
3183
|
-
};
|
|
3184
|
-
await this.mtlsAdapter.configureCertificate(certificateData);
|
|
3185
|
-
}
|
|
3186
|
-
async storeCertificate(certificate, privateKey, options = {}) {
|
|
3187
|
-
if (!this.certificatePort) {
|
|
3188
|
-
throw new Error('Certificate port not available');
|
|
3189
|
-
}
|
|
3190
|
-
if (this.mtlsAdapter) {
|
|
3191
|
-
try {
|
|
3192
|
-
await this.mtlsAdapter.removeCertificate();
|
|
3193
|
-
}
|
|
3194
|
-
catch {
|
|
3195
|
-
// No existing certificate to remove
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
const format = (options.format || 'pem');
|
|
3199
|
-
await this.certificatePort.storeCertificate(certificate, privateKey, format);
|
|
3200
|
-
if (this.mtlsAdapter) {
|
|
3201
|
-
const certificateData = {
|
|
3202
|
-
certificate,
|
|
3203
|
-
privateKey,
|
|
3204
|
-
format: format.toUpperCase(),
|
|
3205
|
-
};
|
|
3206
|
-
await this.mtlsAdapter.configureCertificate(certificateData);
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
async clearCertificate() {
|
|
3210
|
-
if (this.mtlsAdapter) {
|
|
3211
|
-
try {
|
|
3212
|
-
await this.mtlsAdapter.removeCertificate();
|
|
3213
|
-
}
|
|
3214
|
-
catch {
|
|
3215
|
-
// No certificate to remove
|
|
3216
|
-
}
|
|
3217
|
-
}
|
|
3218
|
-
if (this.certificatePort) {
|
|
3219
|
-
await this.certificatePort.clearCertificate();
|
|
3220
|
-
}
|
|
3221
|
-
}
|
|
3222
|
-
async testConnection() {
|
|
3223
|
-
if (!this.mtlsAdapter) {
|
|
3224
|
-
return false;
|
|
3225
|
-
}
|
|
3226
|
-
return this.mtlsAdapter.testConnection();
|
|
3227
|
-
}
|
|
3228
|
-
getBaseUrl() {
|
|
3229
|
-
if (!this.mtlsAdapter) {
|
|
3230
|
-
return null;
|
|
3231
|
-
}
|
|
3232
|
-
return this.mtlsAdapter.getBaseUrl();
|
|
3233
|
-
}
|
|
3234
|
-
async getStatus() {
|
|
3235
|
-
const status = {
|
|
3236
|
-
adapterAvailable: !!this.mtlsAdapter,
|
|
3237
|
-
certificatePortAvailable: !!this.certificatePort,
|
|
3238
|
-
isReady: false,
|
|
3239
|
-
hasCertificate: false,
|
|
3240
|
-
certificateInfo: null,
|
|
3241
|
-
platformInfo: this.mtlsAdapter?.getPlatformInfo() || null,
|
|
3242
|
-
pendingRequestsCount: this.pendingRequests.size,
|
|
3243
|
-
};
|
|
3244
|
-
if (this.certificatePort) {
|
|
3245
|
-
try {
|
|
3246
|
-
status.hasCertificate = await this.certificatePort.hasCertificate();
|
|
3247
|
-
if (status.hasCertificate) {
|
|
3248
|
-
status.certificateInfo = await this.certificatePort.getCertificateInfo();
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3251
|
-
catch {
|
|
3252
|
-
// Ignore errors
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
status.isReady = await this.isMtlsReady();
|
|
3256
|
-
return status;
|
|
3257
|
-
}
|
|
3258
|
-
clearPendingRequests() {
|
|
3259
|
-
this.pendingRequests.clear();
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
const DEFAULT_QUEUE_CONFIG = {
|
|
3264
|
-
maxRetries: 3,
|
|
3265
|
-
retryDelay: 1000,
|
|
3266
|
-
maxRetryDelay: 30000,
|
|
3267
|
-
backoffMultiplier: 2,
|
|
3268
|
-
maxQueueSize: 1000,
|
|
3269
|
-
batchSize: 10,
|
|
3270
|
-
syncInterval: 30000,
|
|
3271
|
-
};
|
|
3272
|
-
|
|
3273
|
-
class OperationQueue {
|
|
3274
|
-
constructor(storage, config = DEFAULT_QUEUE_CONFIG, events = {}) {
|
|
3275
|
-
this.storage = storage;
|
|
3276
|
-
this.config = config;
|
|
3277
|
-
this.events = events;
|
|
3278
|
-
this.queue = [];
|
|
3279
|
-
this.processing = false;
|
|
3280
|
-
this.config = { ...DEFAULT_QUEUE_CONFIG, ...config };
|
|
3281
|
-
this.loadQueue();
|
|
3282
|
-
if (this.config.syncInterval > 0) {
|
|
3283
|
-
this.startAutoSync();
|
|
3284
|
-
}
|
|
3285
|
-
}
|
|
3286
|
-
async addOperation(type, resource, endpoint, method, data, priority = 1) {
|
|
3287
|
-
if (this.queue.length >= this.config.maxQueueSize) {
|
|
3288
|
-
const lowPriorityIndex = this.queue.findIndex((op) => op.priority === 1);
|
|
3289
|
-
if (lowPriorityIndex !== -1) {
|
|
3290
|
-
this.queue.splice(lowPriorityIndex, 1);
|
|
3291
|
-
}
|
|
3292
|
-
else {
|
|
3293
|
-
throw new Error('Queue is full');
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
const operation = {
|
|
3297
|
-
id: this.generateId(),
|
|
3298
|
-
type,
|
|
3299
|
-
resource,
|
|
3300
|
-
endpoint,
|
|
3301
|
-
method,
|
|
3302
|
-
data,
|
|
3303
|
-
status: 'pending',
|
|
3304
|
-
createdAt: Date.now(),
|
|
3305
|
-
updatedAt: Date.now(),
|
|
3306
|
-
retryCount: 0,
|
|
3307
|
-
maxRetries: this.config.maxRetries,
|
|
3308
|
-
priority,
|
|
3309
|
-
};
|
|
3310
|
-
const insertIndex = this.queue.findIndex((op) => op.priority < priority);
|
|
3311
|
-
if (insertIndex === -1) {
|
|
3312
|
-
this.queue.push(operation);
|
|
3313
|
-
}
|
|
3314
|
-
else {
|
|
3315
|
-
this.queue.splice(insertIndex, 0, operation);
|
|
3316
|
-
}
|
|
3317
|
-
await this.saveQueue();
|
|
3318
|
-
this.events.onOperationAdded?.(operation);
|
|
3319
|
-
return operation.id;
|
|
3320
|
-
}
|
|
3321
|
-
getPendingOperations() {
|
|
3322
|
-
return this.queue.filter((op) => op.status === 'pending' || op.status === 'failed');
|
|
3323
|
-
}
|
|
3324
|
-
getOperation(id) {
|
|
3325
|
-
return this.queue.find((op) => op.id === id);
|
|
3326
|
-
}
|
|
3327
|
-
async removeOperation(id) {
|
|
3328
|
-
const index = this.queue.findIndex((op) => op.id === id);
|
|
3329
|
-
if (index === -1)
|
|
3330
|
-
return false;
|
|
3331
|
-
this.queue.splice(index, 1);
|
|
3332
|
-
await this.saveQueue();
|
|
3333
|
-
return true;
|
|
3334
|
-
}
|
|
3335
|
-
async updateOperation(id, updates) {
|
|
3336
|
-
const operation = this.queue.find((op) => op.id === id);
|
|
3337
|
-
if (!operation)
|
|
3338
|
-
return false;
|
|
3339
|
-
Object.assign(operation, { ...updates, updatedAt: Date.now() });
|
|
3340
|
-
await this.saveQueue();
|
|
3341
|
-
return true;
|
|
3342
|
-
}
|
|
3343
|
-
getStats() {
|
|
3344
|
-
return {
|
|
3345
|
-
total: this.queue.length,
|
|
3346
|
-
pending: this.queue.filter((op) => op.status === 'pending').length,
|
|
3347
|
-
processing: this.queue.filter((op) => op.status === 'processing').length,
|
|
3348
|
-
completed: this.queue.filter((op) => op.status === 'completed').length,
|
|
3349
|
-
failed: this.queue.filter((op) => op.status === 'failed').length,
|
|
3350
|
-
};
|
|
3351
|
-
}
|
|
3352
|
-
async clearQueue() {
|
|
3353
|
-
this.queue = [];
|
|
3354
|
-
await this.saveQueue();
|
|
3355
|
-
}
|
|
3356
|
-
async clearCompleted() {
|
|
3357
|
-
this.queue = this.queue.filter((op) => op.status !== 'completed');
|
|
3358
|
-
await this.saveQueue();
|
|
3359
|
-
}
|
|
3360
|
-
async clearFailed() {
|
|
3361
|
-
this.queue = this.queue.filter((op) => op.status !== 'failed');
|
|
3362
|
-
await this.saveQueue();
|
|
3363
|
-
}
|
|
3364
|
-
async retryFailed() {
|
|
3365
|
-
for (const operation of this.queue.filter((op) => op.status === 'failed')) {
|
|
3366
|
-
if (operation.retryCount < operation.maxRetries) {
|
|
3367
|
-
operation.status = 'pending';
|
|
3368
|
-
operation.retryCount++;
|
|
3369
|
-
operation.updatedAt = Date.now();
|
|
3370
|
-
delete operation.error;
|
|
3371
|
-
}
|
|
3372
|
-
}
|
|
3373
|
-
await this.saveQueue();
|
|
3374
|
-
}
|
|
3375
|
-
getNextBatch() {
|
|
3376
|
-
return this.queue
|
|
3377
|
-
.filter((op) => op.status === 'pending')
|
|
3378
|
-
.sort((a, b) => b.priority - a.priority || a.createdAt - b.createdAt)
|
|
3379
|
-
.slice(0, this.config.batchSize);
|
|
3380
|
-
}
|
|
3381
|
-
isEmpty() {
|
|
3382
|
-
return this.getPendingOperations().length === 0;
|
|
3383
|
-
}
|
|
3384
|
-
startAutoSync() {
|
|
3385
|
-
if (this.syncIntervalId)
|
|
3386
|
-
return;
|
|
3387
|
-
this.syncIntervalId = setInterval(() => {
|
|
3388
|
-
if (!this.isEmpty() && !this.processing) {
|
|
3389
|
-
this.events.onQueueEmpty?.();
|
|
3390
|
-
}
|
|
3391
|
-
}, this.config.syncInterval);
|
|
3392
|
-
}
|
|
3393
|
-
stopAutoSync() {
|
|
3394
|
-
if (this.syncIntervalId) {
|
|
3395
|
-
clearInterval(this.syncIntervalId);
|
|
3396
|
-
this.syncIntervalId = undefined;
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
setProcessing(value) {
|
|
3400
|
-
this.processing = value;
|
|
3401
|
-
}
|
|
3402
|
-
isCurrentlyProcessing() {
|
|
3403
|
-
return this.processing;
|
|
3404
|
-
}
|
|
3405
|
-
async loadQueue() {
|
|
3406
|
-
try {
|
|
3407
|
-
const queueData = await this.storage.get(OperationQueue.QUEUE_KEY);
|
|
3408
|
-
if (queueData) {
|
|
3409
|
-
this.queue = JSON.parse(queueData);
|
|
3410
|
-
this.queue.forEach((op) => {
|
|
3411
|
-
if (op.status === 'processing') {
|
|
3412
|
-
op.status = 'pending';
|
|
3413
|
-
}
|
|
3414
|
-
});
|
|
3415
|
-
}
|
|
3416
|
-
}
|
|
3417
|
-
catch {
|
|
3418
|
-
this.queue = [];
|
|
3419
|
-
}
|
|
3420
|
-
}
|
|
3421
|
-
async saveQueue() {
|
|
3422
|
-
try {
|
|
3423
|
-
await this.storage.set(OperationQueue.QUEUE_KEY, JSON.stringify(this.queue));
|
|
3424
|
-
}
|
|
3425
|
-
catch (error) {
|
|
3426
|
-
this.events.onError?.(new Error(`Failed to save queue: ${error}`));
|
|
3427
|
-
}
|
|
3428
|
-
}
|
|
3429
|
-
generateId() {
|
|
3430
|
-
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
3431
|
-
}
|
|
3432
|
-
destroy() {
|
|
3433
|
-
this.stopAutoSync();
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
OperationQueue.QUEUE_KEY = 'acube_operation_queue';
|
|
3437
|
-
|
|
3438
|
-
class SyncManager {
|
|
3439
|
-
constructor(queue, httpPort, networkMonitor, config, events = {}) {
|
|
3440
|
-
this.queue = queue;
|
|
3441
|
-
this.httpPort = httpPort;
|
|
3442
|
-
this.networkMonitor = networkMonitor;
|
|
3443
|
-
this.config = config;
|
|
3444
|
-
this.events = events;
|
|
3445
|
-
this.isOnline = true;
|
|
3446
|
-
this.destroy$ = new Subject();
|
|
3447
|
-
this.setupNetworkMonitoring();
|
|
3448
|
-
}
|
|
3449
|
-
setupNetworkMonitoring() {
|
|
3450
|
-
// Subscribe to online$ to track current state
|
|
3451
|
-
this.networkSubscription = this.networkMonitor.online$
|
|
3452
|
-
.pipe(startWith(true), // Assume online initially
|
|
3453
|
-
pairwise(), filter(([wasOnline, isNowOnline]) => !wasOnline && isNowOnline), takeUntil(this.destroy$))
|
|
3454
|
-
.subscribe(() => {
|
|
3455
|
-
// Offline → Online transition detected
|
|
3456
|
-
this.syncPendingOperations();
|
|
3457
|
-
});
|
|
3458
|
-
// Track current online state
|
|
3459
|
-
this.networkMonitor.online$.pipe(takeUntil(this.destroy$)).subscribe((online) => {
|
|
3460
|
-
this.isOnline = online;
|
|
3461
|
-
});
|
|
3462
|
-
}
|
|
3463
|
-
async syncPendingOperations() {
|
|
3464
|
-
if (!this.isOnline) {
|
|
3465
|
-
throw new Error('Cannot sync while offline');
|
|
3466
|
-
}
|
|
3467
|
-
if (this.queue.isCurrentlyProcessing()) {
|
|
3468
|
-
throw new Error('Sync already in progress');
|
|
3469
|
-
}
|
|
3470
|
-
this.queue.setProcessing(true);
|
|
3471
|
-
try {
|
|
3472
|
-
const results = [];
|
|
3473
|
-
let successCount = 0;
|
|
3474
|
-
let failureCount = 0;
|
|
3475
|
-
while (!this.queue.isEmpty()) {
|
|
3476
|
-
const batch = this.queue.getNextBatch();
|
|
3477
|
-
if (batch.length === 0)
|
|
3478
|
-
break;
|
|
3479
|
-
const batchPromises = batch.map((operation) => this.processOperation(operation));
|
|
3480
|
-
const batchResults = await Promise.allSettled(batchPromises);
|
|
3481
|
-
batchResults.forEach((result, index) => {
|
|
3482
|
-
const operation = batch[index];
|
|
3483
|
-
if (!operation)
|
|
3484
|
-
return;
|
|
3485
|
-
if (result.status === 'fulfilled') {
|
|
3486
|
-
const syncResult = result.value;
|
|
3487
|
-
results.push(syncResult);
|
|
3488
|
-
if (syncResult.success) {
|
|
3489
|
-
successCount++;
|
|
3490
|
-
this.events.onOperationCompleted?.(syncResult);
|
|
3491
|
-
}
|
|
3492
|
-
else {
|
|
3493
|
-
failureCount++;
|
|
3494
|
-
this.events.onOperationFailed?.(syncResult);
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
else {
|
|
3498
|
-
const syncResult = {
|
|
3499
|
-
operation,
|
|
3500
|
-
success: false,
|
|
3501
|
-
error: result.reason?.message || 'Unknown error',
|
|
3502
|
-
};
|
|
3503
|
-
results.push(syncResult);
|
|
3504
|
-
failureCount++;
|
|
3505
|
-
this.events.onOperationFailed?.(syncResult);
|
|
3506
|
-
this.queue.updateOperation(operation.id, {
|
|
3507
|
-
status: 'failed',
|
|
3508
|
-
error: syncResult.error,
|
|
3509
|
-
});
|
|
3510
|
-
}
|
|
3511
|
-
});
|
|
3512
|
-
if (!this.queue.isEmpty()) {
|
|
3513
|
-
await this.delay(500);
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
const batchResult = {
|
|
3517
|
-
totalOperations: results.length,
|
|
3518
|
-
successCount,
|
|
3519
|
-
failureCount,
|
|
3520
|
-
results,
|
|
3521
|
-
};
|
|
3522
|
-
this.events.onBatchSyncCompleted?.(batchResult);
|
|
3523
|
-
if (this.queue.isEmpty()) {
|
|
3524
|
-
this.events.onQueueEmpty?.();
|
|
3525
|
-
}
|
|
3526
|
-
return batchResult;
|
|
3527
|
-
}
|
|
3528
|
-
finally {
|
|
3529
|
-
this.queue.setProcessing(false);
|
|
3530
|
-
}
|
|
3531
|
-
}
|
|
3532
|
-
async processOperation(operation) {
|
|
3533
|
-
await this.queue.updateOperation(operation.id, { status: 'processing' });
|
|
3534
|
-
try {
|
|
3535
|
-
const response = await this.executeOperation(operation);
|
|
3536
|
-
await this.queue.updateOperation(operation.id, { status: 'completed' });
|
|
3537
|
-
return { operation, success: true, response };
|
|
3538
|
-
}
|
|
3539
|
-
catch (error) {
|
|
3540
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
3541
|
-
if (operation.retryCount < operation.maxRetries && this.isRetryableError(error)) {
|
|
3542
|
-
const delay = this.calculateRetryDelay(operation.retryCount);
|
|
3543
|
-
await this.queue.updateOperation(operation.id, {
|
|
3544
|
-
status: 'pending',
|
|
3545
|
-
retryCount: operation.retryCount + 1,
|
|
3546
|
-
error: errorMessage,
|
|
3547
|
-
});
|
|
3548
|
-
setTimeout(() => {
|
|
3549
|
-
if (this.isOnline && !this.queue.isCurrentlyProcessing()) {
|
|
3550
|
-
this.syncPendingOperations();
|
|
3551
|
-
}
|
|
3552
|
-
}, delay);
|
|
3553
|
-
return { operation, success: false, error: `Retrying: ${errorMessage}` };
|
|
3554
|
-
}
|
|
3555
|
-
else {
|
|
3556
|
-
await this.queue.updateOperation(operation.id, {
|
|
3557
|
-
status: 'failed',
|
|
3558
|
-
error: errorMessage,
|
|
3559
|
-
});
|
|
3560
|
-
return { operation, success: false, error: errorMessage };
|
|
3561
|
-
}
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
|
-
async executeOperation(operation) {
|
|
3565
|
-
const { method, endpoint, data, headers } = operation;
|
|
3566
|
-
const config = headers ? { headers } : undefined;
|
|
3567
|
-
switch (method) {
|
|
3568
|
-
case 'GET':
|
|
3569
|
-
return (await this.httpPort.get(endpoint, config)).data;
|
|
3570
|
-
case 'POST':
|
|
3571
|
-
return (await this.httpPort.post(endpoint, data, config)).data;
|
|
3572
|
-
case 'PUT':
|
|
3573
|
-
return (await this.httpPort.put(endpoint, data, config)).data;
|
|
3574
|
-
case 'PATCH':
|
|
3575
|
-
return (await this.httpPort.patch(endpoint, data, config)).data;
|
|
3576
|
-
case 'DELETE':
|
|
3577
|
-
return (await this.httpPort.delete(endpoint, config)).data;
|
|
3578
|
-
default:
|
|
3579
|
-
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
isRetryableError(error) {
|
|
3583
|
-
const errorObj = error;
|
|
3584
|
-
if (errorObj.code === 'NETWORK_ERROR')
|
|
3585
|
-
return true;
|
|
3586
|
-
if (errorObj.statusCode && errorObj.statusCode >= 500)
|
|
3587
|
-
return true;
|
|
3588
|
-
if (errorObj.statusCode === 429)
|
|
3589
|
-
return true;
|
|
3590
|
-
const errorMessage = error?.message;
|
|
3591
|
-
if (errorObj.code === 'ECONNABORTED' || errorMessage?.includes('timeout'))
|
|
3592
|
-
return true;
|
|
3593
|
-
return false;
|
|
3594
|
-
}
|
|
3595
|
-
calculateRetryDelay(retryCount) {
|
|
3596
|
-
const delay = this.config.retryDelay * Math.pow(this.config.backoffMultiplier, retryCount);
|
|
3597
|
-
return Math.min(delay, this.config.maxRetryDelay);
|
|
3598
|
-
}
|
|
3599
|
-
delay(ms) {
|
|
3600
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3601
|
-
}
|
|
3602
|
-
isCurrentlyOnline() {
|
|
3603
|
-
return this.isOnline;
|
|
3604
|
-
}
|
|
3605
|
-
async triggerSync() {
|
|
3606
|
-
if (!this.isOnline)
|
|
3607
|
-
return null;
|
|
3608
|
-
if (this.queue.isEmpty()) {
|
|
3609
|
-
return { totalOperations: 0, successCount: 0, failureCount: 0, results: [] };
|
|
3610
|
-
}
|
|
3611
|
-
return await this.syncPendingOperations();
|
|
3612
|
-
}
|
|
3613
|
-
getSyncStatus() {
|
|
3614
|
-
return {
|
|
3615
|
-
isOnline: this.isOnline,
|
|
3616
|
-
isProcessing: this.queue.isCurrentlyProcessing(),
|
|
3617
|
-
queueStats: this.queue.getStats(),
|
|
3618
|
-
};
|
|
3619
|
-
}
|
|
3620
|
-
destroy() {
|
|
3621
|
-
this.destroy$.next();
|
|
3622
|
-
this.destroy$.complete();
|
|
3623
|
-
this.networkSubscription?.unsubscribe();
|
|
3624
|
-
}
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
class OfflineManager {
|
|
3628
|
-
get queue$() {
|
|
3629
|
-
return this.queueSubject.asObservable();
|
|
3630
|
-
}
|
|
3631
|
-
get syncStatus$() {
|
|
3632
|
-
return this.syncStatusSubject.asObservable();
|
|
3633
|
-
}
|
|
3634
|
-
constructor(storage, httpPort, networkMonitor, config = {}, events = {}, _cache) {
|
|
3635
|
-
this.queueSubject = new BehaviorSubject([]);
|
|
3636
|
-
this.syncStatusSubject = new BehaviorSubject({
|
|
3637
|
-
isOnline: true,
|
|
3638
|
-
isProcessing: false,
|
|
3639
|
-
queueStats: { total: 0, pending: 0, processing: 0, completed: 0, failed: 0 },
|
|
3640
|
-
});
|
|
3641
|
-
this.destroy$ = new Subject();
|
|
3642
|
-
const finalConfig = { ...DEFAULT_QUEUE_CONFIG, ...config };
|
|
3643
|
-
const wrappedEvents = {
|
|
3644
|
-
...events,
|
|
3645
|
-
onOperationAdded: (op) => {
|
|
3646
|
-
this.updateQueueState();
|
|
3647
|
-
events.onOperationAdded?.(op);
|
|
3648
|
-
},
|
|
3649
|
-
onOperationCompleted: (result) => {
|
|
3650
|
-
this.updateQueueState();
|
|
3651
|
-
events.onOperationCompleted?.(result);
|
|
3652
|
-
},
|
|
3653
|
-
onOperationFailed: (result) => {
|
|
3654
|
-
this.updateQueueState();
|
|
3655
|
-
events.onOperationFailed?.(result);
|
|
3656
|
-
},
|
|
3657
|
-
onBatchSyncCompleted: (result) => {
|
|
3658
|
-
this.updateQueueState();
|
|
3659
|
-
events.onBatchSyncCompleted?.(result);
|
|
3660
|
-
},
|
|
3661
|
-
};
|
|
3662
|
-
this.queue = new OperationQueue(storage, finalConfig, wrappedEvents);
|
|
3663
|
-
this.syncManager = new SyncManager(this.queue, httpPort, networkMonitor, finalConfig, wrappedEvents);
|
|
3664
|
-
this.updateQueueState();
|
|
3665
|
-
}
|
|
3666
|
-
updateQueueState() {
|
|
3667
|
-
this.queueSubject.next(this.queue.getPendingOperations());
|
|
3668
|
-
this.syncStatusSubject.next(this.syncManager.getSyncStatus());
|
|
3669
|
-
}
|
|
3670
|
-
async queueOperation(type, resource, endpoint, method, data, priority = 1) {
|
|
3671
|
-
const id = await this.queue.addOperation(type, resource, endpoint, method, data, priority);
|
|
3672
|
-
this.updateQueueState();
|
|
3673
|
-
return id;
|
|
3674
|
-
}
|
|
3675
|
-
async queueReceiptCreation(receiptData, priority = 2) {
|
|
3676
|
-
return await this.queueOperation('CREATE', 'receipt', '/mf1/receipts', 'POST', receiptData, priority);
|
|
3677
|
-
}
|
|
3678
|
-
async queueReceiptVoid(voidData, priority = 3) {
|
|
3679
|
-
return await this.queueOperation('DELETE', 'receipt', '/mf1/receipts', 'DELETE', voidData, priority);
|
|
3680
|
-
}
|
|
3681
|
-
async queueReceiptReturn(returnData, priority = 3) {
|
|
3682
|
-
return await this.queueOperation('CREATE', 'receipt', '/mf1/receipts/return', 'POST', returnData, priority);
|
|
3683
|
-
}
|
|
3684
|
-
async queueCashierCreation(cashierData, priority = 1) {
|
|
3685
|
-
return await this.queueOperation('CREATE', 'cashier', '/mf1/cashiers', 'POST', cashierData, priority);
|
|
3686
|
-
}
|
|
3687
|
-
isOnline() {
|
|
3688
|
-
return this.syncManager.isCurrentlyOnline();
|
|
3689
|
-
}
|
|
3690
|
-
getStatus() {
|
|
3691
|
-
return this.syncManager.getSyncStatus();
|
|
3692
|
-
}
|
|
3693
|
-
getPendingCount() {
|
|
3694
|
-
return this.queue.getPendingOperations().length;
|
|
3695
|
-
}
|
|
3696
|
-
isEmpty() {
|
|
3697
|
-
return this.queue.isEmpty();
|
|
3698
|
-
}
|
|
3699
|
-
async sync() {
|
|
3700
|
-
const result = await this.syncManager.triggerSync();
|
|
3701
|
-
this.updateQueueState();
|
|
3702
|
-
return result;
|
|
3703
|
-
}
|
|
3704
|
-
async retryFailed() {
|
|
3705
|
-
await this.queue.retryFailed();
|
|
3706
|
-
this.updateQueueState();
|
|
3707
|
-
if (this.isOnline()) {
|
|
3708
|
-
await this.sync();
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
async clearCompleted() {
|
|
3712
|
-
await this.queue.clearCompleted();
|
|
3713
|
-
this.updateQueueState();
|
|
3714
|
-
}
|
|
3715
|
-
async clearFailed() {
|
|
3716
|
-
await this.queue.clearFailed();
|
|
3717
|
-
this.updateQueueState();
|
|
3718
|
-
}
|
|
3719
|
-
async clearAll() {
|
|
3720
|
-
await this.queue.clearQueue();
|
|
3721
|
-
this.updateQueueState();
|
|
3722
|
-
}
|
|
3723
|
-
getOperation(id) {
|
|
3724
|
-
return this.queue.getOperation(id);
|
|
3725
|
-
}
|
|
3726
|
-
async removeOperation(id) {
|
|
3727
|
-
const result = await this.queue.removeOperation(id);
|
|
3728
|
-
this.updateQueueState();
|
|
3729
|
-
return result;
|
|
3730
|
-
}
|
|
3731
|
-
getQueueStats() {
|
|
3732
|
-
return this.queue.getStats();
|
|
3733
|
-
}
|
|
3734
|
-
startAutoSync() {
|
|
3735
|
-
this.queue.startAutoSync();
|
|
3736
|
-
}
|
|
3737
|
-
stopAutoSync() {
|
|
3738
|
-
this.queue.stopAutoSync();
|
|
3739
|
-
}
|
|
3740
|
-
destroy() {
|
|
3741
|
-
this.destroy$.next();
|
|
3742
|
-
this.destroy$.complete();
|
|
3743
|
-
this.queue.destroy();
|
|
3744
|
-
this.syncManager.destroy();
|
|
3745
|
-
}
|
|
3746
|
-
}
|
|
3747
|
-
|
|
3748
|
-
class CompressionAdapter {
|
|
3749
|
-
compress(data, threshold = 1024) {
|
|
3750
|
-
const originalSize = data.length * 2;
|
|
3751
|
-
if (originalSize < threshold) {
|
|
3752
|
-
return {
|
|
3753
|
-
data,
|
|
3754
|
-
compressed: false,
|
|
3755
|
-
originalSize,
|
|
3756
|
-
compressedSize: originalSize,
|
|
3757
|
-
};
|
|
3758
|
-
}
|
|
3759
|
-
try {
|
|
3760
|
-
const compressed = this.compressString(data);
|
|
3761
|
-
const compressedSize = compressed.length * 2;
|
|
3762
|
-
if (compressedSize < originalSize) {
|
|
3763
|
-
return {
|
|
3764
|
-
data: compressed,
|
|
3765
|
-
compressed: true,
|
|
3766
|
-
originalSize,
|
|
3767
|
-
compressedSize,
|
|
3768
|
-
};
|
|
3769
|
-
}
|
|
3770
|
-
return {
|
|
3771
|
-
data,
|
|
3772
|
-
compressed: false,
|
|
3773
|
-
originalSize,
|
|
3774
|
-
compressedSize: originalSize,
|
|
3775
|
-
};
|
|
3776
|
-
}
|
|
3777
|
-
catch {
|
|
3778
|
-
return {
|
|
3779
|
-
data,
|
|
3780
|
-
compressed: false,
|
|
3781
|
-
originalSize,
|
|
3782
|
-
compressedSize: originalSize,
|
|
3783
|
-
};
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
decompress(data, compressed) {
|
|
3787
|
-
if (!compressed) {
|
|
3788
|
-
return { data, wasCompressed: false };
|
|
3789
|
-
}
|
|
3790
|
-
try {
|
|
3791
|
-
const decompressed = this.decompressString(data);
|
|
3792
|
-
return { data: decompressed, wasCompressed: true };
|
|
3793
|
-
}
|
|
3794
|
-
catch {
|
|
3795
|
-
return { data, wasCompressed: false };
|
|
3796
|
-
}
|
|
3797
|
-
}
|
|
3798
|
-
estimateSavings(data) {
|
|
3799
|
-
const repeated = data.match(/(.)\1{3,}/g);
|
|
3800
|
-
if (!repeated)
|
|
3801
|
-
return 0;
|
|
3802
|
-
let savings = 0;
|
|
3803
|
-
for (const match of repeated) {
|
|
3804
|
-
const originalBytes = match.length * 2;
|
|
3805
|
-
const compressedBytes = 6;
|
|
3806
|
-
if (originalBytes > compressedBytes) {
|
|
3807
|
-
savings += originalBytes - compressedBytes;
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
return Math.min(savings, data.length * 2 * 0.5);
|
|
3811
|
-
}
|
|
3812
|
-
compressString(input) {
|
|
3813
|
-
let compressed = '';
|
|
3814
|
-
let i = 0;
|
|
3815
|
-
while (i < input.length) {
|
|
3816
|
-
let count = 1;
|
|
3817
|
-
const char = input[i];
|
|
3818
|
-
while (i + count < input.length && input[i + count] === char && count < 255) {
|
|
3819
|
-
count++;
|
|
3820
|
-
}
|
|
3821
|
-
if (count > 3) {
|
|
3822
|
-
compressed += `~${count}${char}`;
|
|
3823
|
-
}
|
|
3824
|
-
else {
|
|
3825
|
-
for (let j = 0; j < count; j++) {
|
|
3826
|
-
compressed += char;
|
|
3827
|
-
}
|
|
3828
|
-
}
|
|
3829
|
-
i += count;
|
|
3830
|
-
}
|
|
3831
|
-
return `COMP:${btoa(compressed)}`;
|
|
3832
|
-
}
|
|
3833
|
-
decompressString(input) {
|
|
3834
|
-
if (!input.startsWith('COMP:')) {
|
|
3835
|
-
return input;
|
|
3836
|
-
}
|
|
3837
|
-
const encodedData = input.substring(5);
|
|
3838
|
-
if (!encodedData) {
|
|
3839
|
-
return input;
|
|
3840
|
-
}
|
|
3841
|
-
const compressed = atob(encodedData);
|
|
3842
|
-
let decompressed = '';
|
|
3843
|
-
let i = 0;
|
|
3844
|
-
while (i < compressed.length) {
|
|
3845
|
-
if (compressed[i] === '~' && i + 2 < compressed.length) {
|
|
3846
|
-
let countStr = '';
|
|
3847
|
-
i++;
|
|
3848
|
-
while (i < compressed.length) {
|
|
3849
|
-
const char = compressed[i];
|
|
3850
|
-
if (char && /\d/.test(char)) {
|
|
3851
|
-
countStr += char;
|
|
3852
|
-
i++;
|
|
3853
|
-
}
|
|
3854
|
-
else {
|
|
3855
|
-
break;
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3858
|
-
if (countStr && i < compressed.length) {
|
|
3859
|
-
const count = parseInt(countStr, 10);
|
|
3860
|
-
const char = compressed[i];
|
|
3861
|
-
for (let j = 0; j < count; j++) {
|
|
3862
|
-
decompressed += char;
|
|
3863
|
-
}
|
|
3864
|
-
i++;
|
|
3865
|
-
}
|
|
3866
|
-
}
|
|
3867
|
-
else {
|
|
3868
|
-
decompressed += compressed[i];
|
|
3869
|
-
i++;
|
|
3870
|
-
}
|
|
3871
|
-
}
|
|
3872
|
-
return decompressed;
|
|
3873
|
-
}
|
|
3874
|
-
}
|
|
3875
|
-
function compressData(data, threshold = 1024) {
|
|
3876
|
-
return new CompressionAdapter().compress(data, threshold);
|
|
3877
|
-
}
|
|
3878
|
-
function decompressData(data, compressed) {
|
|
3879
|
-
return new CompressionAdapter().decompress(data, compressed);
|
|
3880
|
-
}
|
|
3881
|
-
|
|
3882
|
-
const log$e = createPrefixedLogger('CACHE-RN');
|
|
3883
|
-
/**
|
|
3884
|
-
* React Native cache adapter using SQLite (Expo or react-native-sqlite-storage)
|
|
3885
|
-
* Cache never expires - data persists until explicitly invalidated
|
|
3886
|
-
*/
|
|
3887
|
-
class ReactNativeCacheAdapter {
|
|
3888
|
-
constructor(options = {}) {
|
|
3889
|
-
this.db = null;
|
|
3890
|
-
this.initPromise = null;
|
|
3891
|
-
this.isExpo = false;
|
|
3892
|
-
this.hasCompressedColumn = false;
|
|
3893
|
-
this.options = {
|
|
3894
|
-
maxSize: 50 * 1024 * 1024, // 50MB
|
|
3895
|
-
maxEntries: 10000,
|
|
3896
|
-
compression: false,
|
|
3897
|
-
compressionThreshold: 1024,
|
|
3898
|
-
...options,
|
|
3899
|
-
};
|
|
3900
|
-
this.initPromise = this.initialize();
|
|
3901
|
-
}
|
|
3902
|
-
normalizeResults(results) {
|
|
3903
|
-
if (this.isExpo) {
|
|
3904
|
-
const expoResults = results;
|
|
3905
|
-
if (Array.isArray(expoResults)) {
|
|
3906
|
-
return expoResults;
|
|
3907
|
-
}
|
|
3908
|
-
return expoResults.results || [];
|
|
3909
|
-
}
|
|
3910
|
-
else {
|
|
3911
|
-
const rnResults = results;
|
|
3912
|
-
const rows = rnResults.rows;
|
|
3913
|
-
if (!rows || rows.length === 0)
|
|
3914
|
-
return [];
|
|
3915
|
-
const normalizedRows = [];
|
|
3916
|
-
for (let i = 0; i < rows.length; i++) {
|
|
3917
|
-
normalizedRows.push(rows.item(i));
|
|
3918
|
-
}
|
|
3919
|
-
return normalizedRows;
|
|
3920
|
-
}
|
|
3921
|
-
}
|
|
3922
|
-
async initialize() {
|
|
3923
|
-
if (this.db)
|
|
3924
|
-
return;
|
|
3925
|
-
try {
|
|
3926
|
-
// Try Expo SQLite first
|
|
3927
|
-
const ExpoSQLite = require('expo-sqlite');
|
|
3928
|
-
this.db = await ExpoSQLite.openDatabaseAsync(ReactNativeCacheAdapter.DB_NAME);
|
|
3929
|
-
this.isExpo = true;
|
|
3930
|
-
await this.createTables();
|
|
3931
|
-
}
|
|
3932
|
-
catch (expoError) {
|
|
3933
|
-
try {
|
|
3934
|
-
// Fallback to react-native-sqlite-storage
|
|
3935
|
-
const SQLite = require('react-native-sqlite-storage');
|
|
3936
|
-
this.db = await new Promise((resolve, reject) => {
|
|
3937
|
-
SQLite.openDatabase({
|
|
3938
|
-
name: ReactNativeCacheAdapter.DB_NAME,
|
|
3939
|
-
location: 'default',
|
|
3940
|
-
}, resolve, reject);
|
|
3941
|
-
});
|
|
3942
|
-
this.isExpo = false;
|
|
3943
|
-
await this.createTables();
|
|
3944
|
-
}
|
|
3945
|
-
catch (rnError) {
|
|
3946
|
-
throw new Error(`Failed to initialize SQLite: Expo error: ${expoError}, RN error: ${rnError}`);
|
|
3947
|
-
}
|
|
3948
|
-
}
|
|
3949
|
-
}
|
|
3950
|
-
async createTables() {
|
|
3951
|
-
// Create table with simplified schema (no TTL)
|
|
3952
|
-
const createTableSQL = `
|
|
3953
|
-
CREATE TABLE IF NOT EXISTS ${ReactNativeCacheAdapter.TABLE_NAME} (
|
|
3954
|
-
cache_key TEXT PRIMARY KEY,
|
|
3955
|
-
data TEXT NOT NULL,
|
|
3956
|
-
timestamp INTEGER NOT NULL
|
|
3957
|
-
);
|
|
3958
|
-
|
|
3959
|
-
CREATE INDEX IF NOT EXISTS idx_timestamp ON ${ReactNativeCacheAdapter.TABLE_NAME}(timestamp);
|
|
3960
|
-
`;
|
|
3961
|
-
await this.executeSql(createTableSQL);
|
|
3962
|
-
// Then, run migrations to add new columns if they don't exist
|
|
3963
|
-
await this.runMigrations();
|
|
3964
|
-
}
|
|
3965
|
-
async runMigrations() {
|
|
3966
|
-
log$e.debug('Running database migrations...');
|
|
3967
|
-
try {
|
|
3968
|
-
// Check if compressed column exists
|
|
3969
|
-
this.hasCompressedColumn = await this.checkColumnExists('compressed');
|
|
3970
|
-
if (!this.hasCompressedColumn) {
|
|
3971
|
-
log$e.debug('Adding compressed column to cache table');
|
|
3972
|
-
const addColumnSQL = `ALTER TABLE ${ReactNativeCacheAdapter.TABLE_NAME} ADD COLUMN compressed INTEGER DEFAULT 0`;
|
|
3973
|
-
await this.executeSql(addColumnSQL);
|
|
3974
|
-
this.hasCompressedColumn = true;
|
|
3975
|
-
log$e.debug('Successfully added compressed column');
|
|
3976
|
-
}
|
|
3977
|
-
else {
|
|
3978
|
-
log$e.debug('Compressed column already exists');
|
|
3979
|
-
}
|
|
3980
|
-
log$e.debug('Database migrations completed', {
|
|
3981
|
-
hasCompressedColumn: this.hasCompressedColumn,
|
|
3982
|
-
});
|
|
3983
|
-
}
|
|
3984
|
-
catch (error) {
|
|
3985
|
-
log$e.debug('Migration failed, disabling compression features', error);
|
|
3986
|
-
this.hasCompressedColumn = false;
|
|
3987
|
-
// Don't throw - allow the app to continue even if migration fails
|
|
3988
|
-
// The compressed feature will just be disabled
|
|
3989
|
-
}
|
|
3990
|
-
}
|
|
3991
|
-
async checkColumnExists(columnName) {
|
|
3992
|
-
try {
|
|
3993
|
-
const pragmaSQL = `PRAGMA table_info(${ReactNativeCacheAdapter.TABLE_NAME})`;
|
|
3994
|
-
const results = await this.executeSql(pragmaSQL);
|
|
3995
|
-
const columns = this.normalizeResults(results);
|
|
3996
|
-
log$e.debug('Table columns found', { columns: columns.map((c) => c.name) });
|
|
3997
|
-
return columns.some((column) => column.name === columnName);
|
|
3998
|
-
}
|
|
3999
|
-
catch (error) {
|
|
4000
|
-
log$e.debug('Error checking column existence', error);
|
|
4001
|
-
return false;
|
|
4002
|
-
}
|
|
4003
|
-
}
|
|
4004
|
-
async get(key) {
|
|
4005
|
-
await this.ensureInitialized();
|
|
4006
|
-
const sql = `SELECT * FROM ${ReactNativeCacheAdapter.TABLE_NAME} WHERE cache_key = ?`;
|
|
4007
|
-
log$e.debug('Executing get query', { sql, key });
|
|
4008
|
-
const results = await this.executeSql(sql, [key]);
|
|
4009
|
-
log$e.debug('Get query results', { key, hasResults: !!results });
|
|
4010
|
-
// Normalize results from different SQLite implementations
|
|
4011
|
-
const rows = this.normalizeResults(results);
|
|
4012
|
-
if (!rows || rows.length === 0) {
|
|
4013
|
-
return null;
|
|
4014
|
-
}
|
|
4015
|
-
const row = rows[0];
|
|
4016
|
-
if (!row) {
|
|
4017
|
-
return null;
|
|
4018
|
-
}
|
|
4019
|
-
const isCompressed = this.hasCompressedColumn ? !!row.compressed : false;
|
|
4020
|
-
const rawData = isCompressed ? decompressData(row.data, true).data : row.data;
|
|
4021
|
-
return {
|
|
4022
|
-
data: JSON.parse(rawData),
|
|
4023
|
-
timestamp: row.timestamp,
|
|
4024
|
-
compressed: isCompressed,
|
|
4025
|
-
};
|
|
4026
|
-
}
|
|
4027
|
-
async set(key, data) {
|
|
4028
|
-
const item = {
|
|
4029
|
-
data,
|
|
4030
|
-
timestamp: Date.now(),
|
|
4031
|
-
};
|
|
4032
|
-
log$e.debug('Setting cache item', { key });
|
|
4033
|
-
return this.setItem(key, item);
|
|
4034
|
-
}
|
|
4035
|
-
async setItem(key, item) {
|
|
4036
|
-
await this.ensureInitialized();
|
|
4037
|
-
// Handle compression if enabled and compressed column is available
|
|
4038
|
-
const serializedData = JSON.stringify(item.data);
|
|
4039
|
-
let finalData = serializedData;
|
|
4040
|
-
let isCompressed = false;
|
|
4041
|
-
if (this.options.compression && this.options.compressionThreshold && this.hasCompressedColumn) {
|
|
4042
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4043
|
-
finalData = compressionResult.data;
|
|
4044
|
-
isCompressed = compressionResult.compressed;
|
|
4045
|
-
log$e.debug('Compression result', {
|
|
4046
|
-
key,
|
|
4047
|
-
originalSize: compressionResult.originalSize,
|
|
4048
|
-
compressedSize: compressionResult.compressedSize,
|
|
4049
|
-
compressed: isCompressed,
|
|
4050
|
-
savings: compressionResult.originalSize - compressionResult.compressedSize,
|
|
4051
|
-
});
|
|
4052
|
-
}
|
|
4053
|
-
log$e.debug('Setting item with metadata', {
|
|
4054
|
-
key,
|
|
4055
|
-
timestamp: item.timestamp,
|
|
4056
|
-
compressed: isCompressed,
|
|
4057
|
-
hasCompressedColumn: this.hasCompressedColumn,
|
|
4058
|
-
});
|
|
4059
|
-
// Build SQL and parameters based on available columns
|
|
4060
|
-
let sql;
|
|
4061
|
-
let params;
|
|
4062
|
-
if (this.hasCompressedColumn) {
|
|
4063
|
-
sql = `
|
|
4064
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4065
|
-
(cache_key, data, timestamp, compressed)
|
|
4066
|
-
VALUES (?, ?, ?, ?)
|
|
4067
|
-
`;
|
|
4068
|
-
params = [key, finalData, item.timestamp, isCompressed ? 1 : 0];
|
|
4069
|
-
}
|
|
4070
|
-
else {
|
|
4071
|
-
// Fallback for databases without compressed column
|
|
4072
|
-
sql = `
|
|
4073
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4074
|
-
(cache_key, data, timestamp)
|
|
4075
|
-
VALUES (?, ?, ?)
|
|
4076
|
-
`;
|
|
4077
|
-
params = [key, finalData, item.timestamp];
|
|
4078
|
-
}
|
|
4079
|
-
log$e.debug('Executing setItem SQL', { key, paramsCount: params.length });
|
|
4080
|
-
await this.executeSql(sql, params);
|
|
4081
|
-
}
|
|
4082
|
-
async setBatch(items) {
|
|
4083
|
-
if (items.length === 0)
|
|
4084
|
-
return;
|
|
4085
|
-
await this.ensureInitialized();
|
|
4086
|
-
log$e.debug('Batch setting items', { count: items.length });
|
|
4087
|
-
if (this.isExpo) {
|
|
4088
|
-
await this.db.withTransactionAsync(async () => {
|
|
4089
|
-
for (const [key, item] of items) {
|
|
4090
|
-
await this.setBatchItem(key, item);
|
|
4091
|
-
}
|
|
4092
|
-
});
|
|
4093
|
-
}
|
|
4094
|
-
else {
|
|
4095
|
-
return new Promise((resolve, reject) => {
|
|
4096
|
-
this.db.transaction((tx) => {
|
|
4097
|
-
const promises = items.map(([key, item]) => this.setBatchItemRN(tx, key, item));
|
|
4098
|
-
Promise.all(promises)
|
|
4099
|
-
.then(() => resolve())
|
|
4100
|
-
.catch(reject);
|
|
4101
|
-
}, reject, () => resolve());
|
|
4102
|
-
});
|
|
4103
|
-
}
|
|
4104
|
-
log$e.debug('Batch operation completed', { count: items.length });
|
|
4105
|
-
}
|
|
4106
|
-
async setBatchItem(key, item) {
|
|
4107
|
-
// Handle compression if enabled and compressed column is available
|
|
4108
|
-
const serializedData = JSON.stringify(item.data);
|
|
4109
|
-
let finalData = serializedData;
|
|
4110
|
-
let isCompressed = false;
|
|
4111
|
-
if (this.options.compression && this.options.compressionThreshold && this.hasCompressedColumn) {
|
|
4112
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4113
|
-
finalData = compressionResult.data;
|
|
4114
|
-
isCompressed = compressionResult.compressed;
|
|
4115
|
-
}
|
|
4116
|
-
// Build SQL and parameters based on available columns
|
|
4117
|
-
let sql;
|
|
4118
|
-
let params;
|
|
4119
|
-
if (this.hasCompressedColumn) {
|
|
4120
|
-
sql = `
|
|
4121
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4122
|
-
(cache_key, data, timestamp, compressed)
|
|
4123
|
-
VALUES (?, ?, ?, ?)
|
|
4124
|
-
`;
|
|
4125
|
-
params = [key, finalData, item.timestamp, isCompressed ? 1 : 0];
|
|
4126
|
-
}
|
|
4127
|
-
else {
|
|
4128
|
-
sql = `
|
|
4129
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4130
|
-
(cache_key, data, timestamp)
|
|
4131
|
-
VALUES (?, ?, ?)
|
|
4132
|
-
`;
|
|
4133
|
-
params = [key, finalData, item.timestamp];
|
|
4134
|
-
}
|
|
4135
|
-
await this.db.runAsync(sql, params);
|
|
4136
|
-
}
|
|
4137
|
-
async setBatchItemRN(tx, key, item) {
|
|
4138
|
-
// Handle compression if enabled and compressed column is available
|
|
4139
|
-
const serializedData = JSON.stringify(item.data);
|
|
4140
|
-
let finalData = serializedData;
|
|
4141
|
-
let isCompressed = false;
|
|
4142
|
-
if (this.options.compression && this.options.compressionThreshold && this.hasCompressedColumn) {
|
|
4143
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4144
|
-
finalData = compressionResult.data;
|
|
4145
|
-
isCompressed = compressionResult.compressed;
|
|
4146
|
-
}
|
|
4147
|
-
// Build SQL and parameters based on available columns
|
|
4148
|
-
let sql;
|
|
4149
|
-
let params;
|
|
4150
|
-
if (this.hasCompressedColumn) {
|
|
4151
|
-
sql = `
|
|
4152
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4153
|
-
(cache_key, data, timestamp, compressed)
|
|
4154
|
-
VALUES (?, ?, ?, ?)
|
|
4155
|
-
`;
|
|
4156
|
-
params = [key, finalData, item.timestamp, isCompressed ? 1 : 0];
|
|
4157
|
-
}
|
|
4158
|
-
else {
|
|
4159
|
-
sql = `
|
|
4160
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4161
|
-
(cache_key, data, timestamp)
|
|
4162
|
-
VALUES (?, ?, ?)
|
|
4163
|
-
`;
|
|
4164
|
-
params = [key, finalData, item.timestamp];
|
|
4165
|
-
}
|
|
4166
|
-
return new Promise((resolve, reject) => {
|
|
4167
|
-
tx.executeSql(sql, params, () => resolve(), (_, error) => {
|
|
4168
|
-
reject(error);
|
|
4169
|
-
return false;
|
|
4170
|
-
});
|
|
4171
|
-
});
|
|
4172
|
-
}
|
|
4173
|
-
async invalidate(pattern) {
|
|
4174
|
-
await this.ensureInitialized();
|
|
4175
|
-
const keys = await this.getKeys(pattern);
|
|
4176
|
-
if (keys.length === 0)
|
|
4177
|
-
return;
|
|
4178
|
-
const placeholders = keys.map(() => '?').join(',');
|
|
4179
|
-
const sql = `DELETE FROM ${ReactNativeCacheAdapter.TABLE_NAME} WHERE cache_key IN (${placeholders})`;
|
|
4180
|
-
await this.executeSql(sql, keys);
|
|
4181
|
-
}
|
|
4182
|
-
async clear() {
|
|
4183
|
-
await this.ensureInitialized();
|
|
4184
|
-
const sql = `DELETE FROM ${ReactNativeCacheAdapter.TABLE_NAME}`;
|
|
4185
|
-
await this.executeSql(sql);
|
|
4186
|
-
}
|
|
4187
|
-
async getSize() {
|
|
4188
|
-
await this.ensureInitialized();
|
|
4189
|
-
const sql = `
|
|
4190
|
-
SELECT
|
|
4191
|
-
COUNT(*) as entries,
|
|
4192
|
-
SUM(LENGTH(data)) as bytes
|
|
4193
|
-
FROM ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4194
|
-
`;
|
|
4195
|
-
const results = await this.executeSql(sql);
|
|
4196
|
-
const rows = this.normalizeResults(results);
|
|
4197
|
-
const row = rows[0] || { entries: 0, bytes: 0 };
|
|
4198
|
-
return {
|
|
4199
|
-
entries: row.entries || 0,
|
|
4200
|
-
bytes: (row.bytes || 0) * 2,
|
|
4201
|
-
lastCleanup: Date.now(),
|
|
4202
|
-
};
|
|
4203
|
-
}
|
|
4204
|
-
async cleanup() {
|
|
4205
|
-
// No cleanup needed - cache never expires
|
|
4206
|
-
return 0;
|
|
4207
|
-
}
|
|
4208
|
-
async getKeys(pattern) {
|
|
4209
|
-
await this.ensureInitialized();
|
|
4210
|
-
let sql = `SELECT cache_key FROM ${ReactNativeCacheAdapter.TABLE_NAME}`;
|
|
4211
|
-
const params = [];
|
|
4212
|
-
if (pattern) {
|
|
4213
|
-
// Simple pattern matching with LIKE
|
|
4214
|
-
const likePattern = pattern.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
4215
|
-
sql += ' WHERE cache_key LIKE ?';
|
|
4216
|
-
params.push(likePattern);
|
|
4217
|
-
}
|
|
4218
|
-
const results = await this.executeSql(sql, params);
|
|
4219
|
-
const keys = [];
|
|
4220
|
-
const rows = this.normalizeResults(results);
|
|
4221
|
-
for (const row of rows) {
|
|
4222
|
-
keys.push(row.cache_key);
|
|
4223
|
-
}
|
|
4224
|
-
return keys;
|
|
4225
|
-
}
|
|
4226
|
-
async executeSql(sql, params = []) {
|
|
4227
|
-
if (this.isExpo) {
|
|
4228
|
-
const expoDB = this.db;
|
|
4229
|
-
if (sql.toLowerCase().includes('select') || sql.toLowerCase().includes('pragma')) {
|
|
4230
|
-
const result = await expoDB.getAllAsync(sql, params);
|
|
4231
|
-
return Array.isArray(result) ? { results: result } : result;
|
|
4232
|
-
}
|
|
4233
|
-
else {
|
|
4234
|
-
return await expoDB.runAsync(sql, params);
|
|
4235
|
-
}
|
|
4236
|
-
}
|
|
4237
|
-
else {
|
|
4238
|
-
// react-native-sqlite-storage
|
|
4239
|
-
return new Promise((resolve, reject) => {
|
|
4240
|
-
this.db.transaction((tx) => {
|
|
4241
|
-
tx.executeSql(sql, params, (_, results) => resolve(results), (_, error) => {
|
|
4242
|
-
reject(error);
|
|
4243
|
-
return false;
|
|
4244
|
-
});
|
|
4245
|
-
});
|
|
4246
|
-
});
|
|
4247
|
-
}
|
|
4248
|
-
}
|
|
4249
|
-
async ensureInitialized() {
|
|
4250
|
-
if (!this.initPromise) {
|
|
4251
|
-
this.initPromise = this.initialize();
|
|
4252
|
-
}
|
|
4253
|
-
await this.initPromise;
|
|
4254
|
-
}
|
|
4255
|
-
}
|
|
4256
|
-
ReactNativeCacheAdapter.DB_NAME = 'acube_cache.db';
|
|
4257
|
-
ReactNativeCacheAdapter.TABLE_NAME = 'cache_entries';
|
|
4258
|
-
/**
|
|
4259
|
-
* Memory-based fallback cache adapter for environments without SQLite
|
|
4260
|
-
* Cache never expires - data persists until explicitly invalidated
|
|
4261
|
-
*/
|
|
4262
|
-
class MemoryCacheAdapter {
|
|
4263
|
-
constructor(options = {}) {
|
|
4264
|
-
this.cache = new Map();
|
|
4265
|
-
this.totalBytes = 0;
|
|
4266
|
-
this.options = {
|
|
4267
|
-
maxEntries: 1000,
|
|
4268
|
-
...options,
|
|
4269
|
-
};
|
|
4270
|
-
}
|
|
4271
|
-
calculateItemSize(key, item) {
|
|
4272
|
-
// Calculate rough size estimation for memory usage
|
|
4273
|
-
const keySize = key.length * 2; // UTF-16 estimation
|
|
4274
|
-
const itemSize = JSON.stringify(item).length * 2; // UTF-16 estimation
|
|
4275
|
-
return keySize + itemSize;
|
|
4276
|
-
}
|
|
4277
|
-
async get(key) {
|
|
4278
|
-
log$e.debug('Getting cache item', { key });
|
|
4279
|
-
const item = this.cache.get(key);
|
|
4280
|
-
if (!item) {
|
|
4281
|
-
log$e.debug('Cache miss', { key });
|
|
4282
|
-
return null;
|
|
4283
|
-
}
|
|
4284
|
-
// Handle decompression if needed
|
|
4285
|
-
const isCompressed = !!item.compressed;
|
|
4286
|
-
let finalData = item.data;
|
|
4287
|
-
if (isCompressed) {
|
|
4288
|
-
const decompressed = decompressData(item.data, true);
|
|
4289
|
-
finalData = JSON.parse(decompressed.data);
|
|
4290
|
-
}
|
|
4291
|
-
log$e.debug('Cache hit', { key, compressed: isCompressed });
|
|
4292
|
-
return {
|
|
4293
|
-
...item,
|
|
4294
|
-
data: finalData,
|
|
4295
|
-
compressed: isCompressed,
|
|
4296
|
-
};
|
|
4297
|
-
}
|
|
4298
|
-
async set(key, data) {
|
|
4299
|
-
log$e.debug('Setting cache item', { key });
|
|
4300
|
-
// Handle compression if enabled
|
|
4301
|
-
let finalData = data;
|
|
4302
|
-
let isCompressed = false;
|
|
4303
|
-
if (this.options.compression && this.options.compressionThreshold) {
|
|
4304
|
-
const serializedData = JSON.stringify(data);
|
|
4305
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4306
|
-
if (compressionResult.compressed) {
|
|
4307
|
-
finalData = compressionResult.data;
|
|
4308
|
-
isCompressed = true;
|
|
4309
|
-
log$e.debug('Compression result', {
|
|
4310
|
-
key,
|
|
4311
|
-
originalSize: compressionResult.originalSize,
|
|
4312
|
-
compressedSize: compressionResult.compressedSize,
|
|
4313
|
-
savings: compressionResult.originalSize - compressionResult.compressedSize,
|
|
4314
|
-
});
|
|
4315
|
-
}
|
|
4316
|
-
}
|
|
4317
|
-
const item = {
|
|
4318
|
-
data: finalData,
|
|
4319
|
-
timestamp: Date.now(),
|
|
4320
|
-
compressed: isCompressed,
|
|
4321
|
-
};
|
|
4322
|
-
return this.setItem(key, item);
|
|
4323
|
-
}
|
|
4324
|
-
async setItem(key, item) {
|
|
4325
|
-
// Calculate size of new item
|
|
4326
|
-
const newItemSize = this.calculateItemSize(key, item);
|
|
4327
|
-
// If item already exists, subtract old size
|
|
4328
|
-
if (this.cache.has(key)) {
|
|
4329
|
-
const oldItem = this.cache.get(key);
|
|
4330
|
-
const oldItemSize = this.calculateItemSize(key, oldItem);
|
|
4331
|
-
this.totalBytes -= oldItemSize;
|
|
4332
|
-
}
|
|
4333
|
-
// Enforce max entries limit
|
|
4334
|
-
if (this.cache.size >= (this.options.maxEntries || 1000) && !this.cache.has(key)) {
|
|
4335
|
-
const oldestKey = this.cache.keys().next().value;
|
|
4336
|
-
if (oldestKey) {
|
|
4337
|
-
const oldestItem = this.cache.get(oldestKey);
|
|
4338
|
-
const oldestItemSize = this.calculateItemSize(oldestKey, oldestItem);
|
|
4339
|
-
this.totalBytes -= oldestItemSize;
|
|
4340
|
-
this.cache.delete(oldestKey);
|
|
4341
|
-
log$e.debug('Removed oldest item for capacity', { oldestKey, freedBytes: oldestItemSize });
|
|
4342
|
-
}
|
|
4343
|
-
}
|
|
4344
|
-
// Set new item and update total size
|
|
4345
|
-
this.cache.set(key, item);
|
|
4346
|
-
this.totalBytes += newItemSize;
|
|
4347
|
-
log$e.debug('Updated cache size', {
|
|
4348
|
-
entries: this.cache.size,
|
|
4349
|
-
totalBytes: this.totalBytes,
|
|
4350
|
-
newItemSize,
|
|
4351
|
-
});
|
|
4352
|
-
}
|
|
4353
|
-
async setBatch(items) {
|
|
4354
|
-
if (items.length === 0)
|
|
4355
|
-
return;
|
|
4356
|
-
log$e.debug('Batch setting items', { count: items.length });
|
|
4357
|
-
let totalNewBytes = 0;
|
|
4358
|
-
let totalOldBytes = 0;
|
|
4359
|
-
const itemsToRemove = [];
|
|
4360
|
-
// First pass: calculate size changes and identify capacity issues
|
|
4361
|
-
for (const [key, item] of items) {
|
|
4362
|
-
const newItemSize = this.calculateItemSize(key, item);
|
|
4363
|
-
totalNewBytes += newItemSize;
|
|
4364
|
-
// If item already exists, track old size for removal
|
|
4365
|
-
if (this.cache.has(key)) {
|
|
4366
|
-
const oldItem = this.cache.get(key);
|
|
4367
|
-
const oldItemSize = this.calculateItemSize(key, oldItem);
|
|
4368
|
-
totalOldBytes += oldItemSize;
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
|
-
// Handle capacity limits - remove oldest items if needed
|
|
4372
|
-
const projectedEntries = this.cache.size + items.filter(([key]) => !this.cache.has(key)).length;
|
|
4373
|
-
const maxEntries = this.options.maxEntries || 1000;
|
|
4374
|
-
if (projectedEntries > maxEntries) {
|
|
4375
|
-
const entriesToRemove = projectedEntries - maxEntries;
|
|
4376
|
-
const oldestKeys = Array.from(this.cache.keys()).slice(0, entriesToRemove);
|
|
4377
|
-
for (const oldKey of oldestKeys) {
|
|
4378
|
-
const oldItem = this.cache.get(oldKey);
|
|
4379
|
-
const oldItemSize = this.calculateItemSize(oldKey, oldItem);
|
|
4380
|
-
this.totalBytes -= oldItemSize;
|
|
4381
|
-
this.cache.delete(oldKey);
|
|
4382
|
-
itemsToRemove.push(oldKey);
|
|
4383
|
-
}
|
|
4384
|
-
if (itemsToRemove.length > 0) {
|
|
4385
|
-
log$e.debug('Removed items for batch capacity', {
|
|
4386
|
-
removedCount: itemsToRemove.length,
|
|
4387
|
-
removedKeys: itemsToRemove,
|
|
4388
|
-
});
|
|
4389
|
-
}
|
|
4390
|
-
}
|
|
4391
|
-
// Update total bytes accounting
|
|
4392
|
-
this.totalBytes = this.totalBytes - totalOldBytes + totalNewBytes;
|
|
4393
|
-
// Second pass: set all items
|
|
4394
|
-
for (const [key, item] of items) {
|
|
4395
|
-
this.cache.set(key, item);
|
|
4396
|
-
}
|
|
4397
|
-
log$e.debug('Batch operation completed', {
|
|
4398
|
-
count: items.length,
|
|
4399
|
-
totalBytes: this.totalBytes,
|
|
4400
|
-
entries: this.cache.size,
|
|
4401
|
-
bytesAdded: totalNewBytes - totalOldBytes,
|
|
4402
|
-
});
|
|
4403
|
-
}
|
|
4404
|
-
async invalidate(pattern) {
|
|
4405
|
-
const regex = this.patternToRegex(pattern);
|
|
4406
|
-
let removed = 0;
|
|
4407
|
-
let bytesFreed = 0;
|
|
4408
|
-
for (const key of this.cache.keys()) {
|
|
4409
|
-
if (regex.test(key)) {
|
|
4410
|
-
const item = this.cache.get(key);
|
|
4411
|
-
const itemSize = this.calculateItemSize(key, item);
|
|
4412
|
-
this.cache.delete(key);
|
|
4413
|
-
this.totalBytes -= itemSize;
|
|
4414
|
-
bytesFreed += itemSize;
|
|
4415
|
-
removed++;
|
|
4416
|
-
}
|
|
4417
|
-
}
|
|
4418
|
-
if (removed > 0) {
|
|
4419
|
-
log$e.debug('Invalidation completed', {
|
|
4420
|
-
pattern,
|
|
4421
|
-
entriesRemoved: removed,
|
|
4422
|
-
bytesFreed,
|
|
4423
|
-
remainingEntries: this.cache.size,
|
|
4424
|
-
remainingBytes: this.totalBytes,
|
|
4425
|
-
});
|
|
4426
|
-
}
|
|
4427
|
-
}
|
|
4428
|
-
async clear() {
|
|
4429
|
-
this.cache.clear();
|
|
4430
|
-
this.totalBytes = 0;
|
|
4431
|
-
log$e.debug('Cache cleared', { entries: 0, bytes: 0 });
|
|
4432
|
-
}
|
|
4433
|
-
async getSize() {
|
|
4434
|
-
return {
|
|
4435
|
-
entries: this.cache.size,
|
|
4436
|
-
bytes: this.totalBytes,
|
|
4437
|
-
lastCleanup: Date.now(),
|
|
4438
|
-
};
|
|
4439
|
-
}
|
|
4440
|
-
async cleanup() {
|
|
4441
|
-
// No cleanup needed - cache never expires
|
|
4442
|
-
return 0;
|
|
4443
|
-
}
|
|
4444
|
-
async getKeys(pattern) {
|
|
4445
|
-
const keys = Array.from(this.cache.keys());
|
|
4446
|
-
if (!pattern)
|
|
4447
|
-
return keys;
|
|
4448
|
-
const regex = this.patternToRegex(pattern);
|
|
4449
|
-
return keys.filter((key) => regex.test(key));
|
|
4450
|
-
}
|
|
4451
|
-
patternToRegex(pattern) {
|
|
4452
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
4453
|
-
const regexPattern = escaped.replace(/\\\*/g, '.*').replace(/\\\?/g, '.');
|
|
4454
|
-
return new RegExp(`^${regexPattern}$`);
|
|
4455
|
-
}
|
|
4456
|
-
}
|
|
4457
|
-
|
|
4458
|
-
const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
|
|
4459
|
-
|
|
4460
|
-
let idbProxyableTypes;
|
|
4461
|
-
let cursorAdvanceMethods;
|
|
4462
|
-
// This is a function to prevent it throwing up in node environments.
|
|
4463
|
-
function getIdbProxyableTypes() {
|
|
4464
|
-
return (idbProxyableTypes ||
|
|
4465
|
-
(idbProxyableTypes = [
|
|
4466
|
-
IDBDatabase,
|
|
4467
|
-
IDBObjectStore,
|
|
4468
|
-
IDBIndex,
|
|
4469
|
-
IDBCursor,
|
|
4470
|
-
IDBTransaction,
|
|
4471
|
-
]));
|
|
4472
|
-
}
|
|
4473
|
-
// This is a function to prevent it throwing up in node environments.
|
|
4474
|
-
function getCursorAdvanceMethods() {
|
|
4475
|
-
return (cursorAdvanceMethods ||
|
|
4476
|
-
(cursorAdvanceMethods = [
|
|
4477
|
-
IDBCursor.prototype.advance,
|
|
4478
|
-
IDBCursor.prototype.continue,
|
|
4479
|
-
IDBCursor.prototype.continuePrimaryKey,
|
|
4480
|
-
]));
|
|
4481
|
-
}
|
|
4482
|
-
const transactionDoneMap = new WeakMap();
|
|
4483
|
-
const transformCache = new WeakMap();
|
|
4484
|
-
const reverseTransformCache = new WeakMap();
|
|
4485
|
-
function promisifyRequest(request) {
|
|
4486
|
-
const promise = new Promise((resolve, reject) => {
|
|
4487
|
-
const unlisten = () => {
|
|
4488
|
-
request.removeEventListener('success', success);
|
|
4489
|
-
request.removeEventListener('error', error);
|
|
4490
|
-
};
|
|
4491
|
-
const success = () => {
|
|
4492
|
-
resolve(wrap(request.result));
|
|
4493
|
-
unlisten();
|
|
4494
|
-
};
|
|
4495
|
-
const error = () => {
|
|
4496
|
-
reject(request.error);
|
|
4497
|
-
unlisten();
|
|
4498
|
-
};
|
|
4499
|
-
request.addEventListener('success', success);
|
|
4500
|
-
request.addEventListener('error', error);
|
|
4501
|
-
});
|
|
4502
|
-
// This mapping exists in reverseTransformCache but doesn't exist in transformCache. This
|
|
4503
|
-
// is because we create many promises from a single IDBRequest.
|
|
4504
|
-
reverseTransformCache.set(promise, request);
|
|
4505
|
-
return promise;
|
|
4506
|
-
}
|
|
4507
|
-
function cacheDonePromiseForTransaction(tx) {
|
|
4508
|
-
// Early bail if we've already created a done promise for this transaction.
|
|
4509
|
-
if (transactionDoneMap.has(tx))
|
|
4510
|
-
return;
|
|
4511
|
-
const done = new Promise((resolve, reject) => {
|
|
4512
|
-
const unlisten = () => {
|
|
4513
|
-
tx.removeEventListener('complete', complete);
|
|
4514
|
-
tx.removeEventListener('error', error);
|
|
4515
|
-
tx.removeEventListener('abort', error);
|
|
4516
|
-
};
|
|
4517
|
-
const complete = () => {
|
|
4518
|
-
resolve();
|
|
4519
|
-
unlisten();
|
|
4520
|
-
};
|
|
4521
|
-
const error = () => {
|
|
4522
|
-
reject(tx.error || new DOMException('AbortError', 'AbortError'));
|
|
4523
|
-
unlisten();
|
|
4524
|
-
};
|
|
4525
|
-
tx.addEventListener('complete', complete);
|
|
4526
|
-
tx.addEventListener('error', error);
|
|
4527
|
-
tx.addEventListener('abort', error);
|
|
4528
|
-
});
|
|
4529
|
-
// Cache it for later retrieval.
|
|
4530
|
-
transactionDoneMap.set(tx, done);
|
|
4531
|
-
}
|
|
4532
|
-
let idbProxyTraps = {
|
|
4533
|
-
get(target, prop, receiver) {
|
|
4534
|
-
if (target instanceof IDBTransaction) {
|
|
4535
|
-
// Special handling for transaction.done.
|
|
4536
|
-
if (prop === 'done')
|
|
4537
|
-
return transactionDoneMap.get(target);
|
|
4538
|
-
// Make tx.store return the only store in the transaction, or undefined if there are many.
|
|
4539
|
-
if (prop === 'store') {
|
|
4540
|
-
return receiver.objectStoreNames[1]
|
|
4541
|
-
? undefined
|
|
4542
|
-
: receiver.objectStore(receiver.objectStoreNames[0]);
|
|
4543
|
-
}
|
|
4544
|
-
}
|
|
4545
|
-
// Else transform whatever we get back.
|
|
4546
|
-
return wrap(target[prop]);
|
|
4547
|
-
},
|
|
4548
|
-
set(target, prop, value) {
|
|
4549
|
-
target[prop] = value;
|
|
4550
|
-
return true;
|
|
4551
|
-
},
|
|
4552
|
-
has(target, prop) {
|
|
4553
|
-
if (target instanceof IDBTransaction &&
|
|
4554
|
-
(prop === 'done' || prop === 'store')) {
|
|
4555
|
-
return true;
|
|
4556
|
-
}
|
|
4557
|
-
return prop in target;
|
|
4558
|
-
},
|
|
4559
|
-
};
|
|
4560
|
-
function replaceTraps(callback) {
|
|
4561
|
-
idbProxyTraps = callback(idbProxyTraps);
|
|
4562
|
-
}
|
|
4563
|
-
function wrapFunction(func) {
|
|
4564
|
-
// Due to expected object equality (which is enforced by the caching in `wrap`), we
|
|
4565
|
-
// only create one new func per func.
|
|
4566
|
-
// Cursor methods are special, as the behaviour is a little more different to standard IDB. In
|
|
4567
|
-
// IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the
|
|
4568
|
-
// cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense
|
|
4569
|
-
// with real promises, so each advance methods returns a new promise for the cursor object, or
|
|
4570
|
-
// undefined if the end of the cursor has been reached.
|
|
4571
|
-
if (getCursorAdvanceMethods().includes(func)) {
|
|
4572
|
-
return function (...args) {
|
|
4573
|
-
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
|
|
4574
|
-
// the original object.
|
|
4575
|
-
func.apply(unwrap(this), args);
|
|
4576
|
-
return wrap(this.request);
|
|
4577
|
-
};
|
|
4578
|
-
}
|
|
4579
|
-
return function (...args) {
|
|
4580
|
-
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
|
|
4581
|
-
// the original object.
|
|
4582
|
-
return wrap(func.apply(unwrap(this), args));
|
|
4583
|
-
};
|
|
4584
|
-
}
|
|
4585
|
-
function transformCachableValue(value) {
|
|
4586
|
-
if (typeof value === 'function')
|
|
4587
|
-
return wrapFunction(value);
|
|
4588
|
-
// This doesn't return, it just creates a 'done' promise for the transaction,
|
|
4589
|
-
// which is later returned for transaction.done (see idbObjectHandler).
|
|
4590
|
-
if (value instanceof IDBTransaction)
|
|
4591
|
-
cacheDonePromiseForTransaction(value);
|
|
4592
|
-
if (instanceOfAny(value, getIdbProxyableTypes()))
|
|
4593
|
-
return new Proxy(value, idbProxyTraps);
|
|
4594
|
-
// Return the same value back if we're not going to transform it.
|
|
4595
|
-
return value;
|
|
4596
|
-
}
|
|
4597
|
-
function wrap(value) {
|
|
4598
|
-
// We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because
|
|
4599
|
-
// IDB is weird and a single IDBRequest can yield many responses, so these can't be cached.
|
|
4600
|
-
if (value instanceof IDBRequest)
|
|
4601
|
-
return promisifyRequest(value);
|
|
4602
|
-
// If we've already transformed this value before, reuse the transformed value.
|
|
4603
|
-
// This is faster, but it also provides object equality.
|
|
4604
|
-
if (transformCache.has(value))
|
|
4605
|
-
return transformCache.get(value);
|
|
4606
|
-
const newValue = transformCachableValue(value);
|
|
4607
|
-
// Not all types are transformed.
|
|
4608
|
-
// These may be primitive types, so they can't be WeakMap keys.
|
|
4609
|
-
if (newValue !== value) {
|
|
4610
|
-
transformCache.set(value, newValue);
|
|
4611
|
-
reverseTransformCache.set(newValue, value);
|
|
4612
|
-
}
|
|
4613
|
-
return newValue;
|
|
4614
|
-
}
|
|
4615
|
-
const unwrap = (value) => reverseTransformCache.get(value);
|
|
4616
|
-
|
|
4617
|
-
/**
|
|
4618
|
-
* Open a database.
|
|
4619
|
-
*
|
|
4620
|
-
* @param name Name of the database.
|
|
4621
|
-
* @param version Schema version.
|
|
4622
|
-
* @param callbacks Additional callbacks.
|
|
4623
|
-
*/
|
|
4624
|
-
function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
|
|
4625
|
-
const request = indexedDB.open(name, version);
|
|
4626
|
-
const openPromise = wrap(request);
|
|
4627
|
-
if (upgrade) {
|
|
4628
|
-
request.addEventListener('upgradeneeded', (event) => {
|
|
4629
|
-
upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
|
|
4630
|
-
});
|
|
4631
|
-
}
|
|
4632
|
-
if (blocked) {
|
|
4633
|
-
request.addEventListener('blocked', (event) => blocked(
|
|
4634
|
-
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
|
|
4635
|
-
event.oldVersion, event.newVersion, event));
|
|
4636
|
-
}
|
|
4637
|
-
openPromise
|
|
4638
|
-
.then((db) => {
|
|
4639
|
-
if (terminated)
|
|
4640
|
-
db.addEventListener('close', () => terminated());
|
|
4641
|
-
if (blocking) {
|
|
4642
|
-
db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event));
|
|
4643
|
-
}
|
|
4644
|
-
})
|
|
4645
|
-
.catch(() => { });
|
|
4646
|
-
return openPromise;
|
|
4647
|
-
}
|
|
4648
|
-
/**
|
|
4649
|
-
* Delete a database.
|
|
4650
|
-
*
|
|
4651
|
-
* @param name Name of the database.
|
|
4652
|
-
*/
|
|
4653
|
-
function deleteDB(name, { blocked } = {}) {
|
|
4654
|
-
const request = indexedDB.deleteDatabase(name);
|
|
4655
|
-
if (blocked) {
|
|
4656
|
-
request.addEventListener('blocked', (event) => blocked(
|
|
4657
|
-
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
|
|
4658
|
-
event.oldVersion, event));
|
|
4659
|
-
}
|
|
4660
|
-
return wrap(request).then(() => undefined);
|
|
4661
|
-
}
|
|
4662
|
-
|
|
4663
|
-
const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'];
|
|
4664
|
-
const writeMethods = ['put', 'add', 'delete', 'clear'];
|
|
4665
|
-
const cachedMethods = new Map();
|
|
4666
|
-
function getMethod(target, prop) {
|
|
4667
|
-
if (!(target instanceof IDBDatabase &&
|
|
4668
|
-
!(prop in target) &&
|
|
4669
|
-
typeof prop === 'string')) {
|
|
4670
|
-
return;
|
|
4671
|
-
}
|
|
4672
|
-
if (cachedMethods.get(prop))
|
|
4673
|
-
return cachedMethods.get(prop);
|
|
4674
|
-
const targetFuncName = prop.replace(/FromIndex$/, '');
|
|
4675
|
-
const useIndex = prop !== targetFuncName;
|
|
4676
|
-
const isWrite = writeMethods.includes(targetFuncName);
|
|
4677
|
-
if (
|
|
4678
|
-
// Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
|
|
4679
|
-
!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) ||
|
|
4680
|
-
!(isWrite || readMethods.includes(targetFuncName))) {
|
|
4681
|
-
return;
|
|
4682
|
-
}
|
|
4683
|
-
const method = async function (storeName, ...args) {
|
|
4684
|
-
// isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :(
|
|
4685
|
-
const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly');
|
|
4686
|
-
let target = tx.store;
|
|
4687
|
-
if (useIndex)
|
|
4688
|
-
target = target.index(args.shift());
|
|
4689
|
-
// Must reject if op rejects.
|
|
4690
|
-
// If it's a write operation, must reject if tx.done rejects.
|
|
4691
|
-
// Must reject with op rejection first.
|
|
4692
|
-
// Must resolve with op value.
|
|
4693
|
-
// Must handle both promises (no unhandled rejections)
|
|
4694
|
-
return (await Promise.all([
|
|
4695
|
-
target[targetFuncName](...args),
|
|
4696
|
-
isWrite && tx.done,
|
|
4697
|
-
]))[0];
|
|
4698
|
-
};
|
|
4699
|
-
cachedMethods.set(prop, method);
|
|
4700
|
-
return method;
|
|
4701
|
-
}
|
|
4702
|
-
replaceTraps((oldTraps) => ({
|
|
4703
|
-
...oldTraps,
|
|
4704
|
-
get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
|
|
4705
|
-
has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop),
|
|
4706
|
-
}));
|
|
4707
|
-
|
|
4708
|
-
const advanceMethodProps = ['continue', 'continuePrimaryKey', 'advance'];
|
|
4709
|
-
const methodMap = {};
|
|
4710
|
-
const advanceResults = new WeakMap();
|
|
4711
|
-
const ittrProxiedCursorToOriginalProxy = new WeakMap();
|
|
4712
|
-
const cursorIteratorTraps = {
|
|
4713
|
-
get(target, prop) {
|
|
4714
|
-
if (!advanceMethodProps.includes(prop))
|
|
4715
|
-
return target[prop];
|
|
4716
|
-
let cachedFunc = methodMap[prop];
|
|
4717
|
-
if (!cachedFunc) {
|
|
4718
|
-
cachedFunc = methodMap[prop] = function (...args) {
|
|
4719
|
-
advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
|
|
4720
|
-
};
|
|
4721
|
-
}
|
|
4722
|
-
return cachedFunc;
|
|
4723
|
-
},
|
|
4724
|
-
};
|
|
4725
|
-
async function* iterate(...args) {
|
|
4726
|
-
// tslint:disable-next-line:no-this-assignment
|
|
4727
|
-
let cursor = this;
|
|
4728
|
-
if (!(cursor instanceof IDBCursor)) {
|
|
4729
|
-
cursor = await cursor.openCursor(...args);
|
|
4730
|
-
}
|
|
4731
|
-
if (!cursor)
|
|
4732
|
-
return;
|
|
4733
|
-
cursor = cursor;
|
|
4734
|
-
const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
|
|
4735
|
-
ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
|
|
4736
|
-
// Map this double-proxy back to the original, so other cursor methods work.
|
|
4737
|
-
reverseTransformCache.set(proxiedCursor, unwrap(cursor));
|
|
4738
|
-
while (cursor) {
|
|
4739
|
-
yield proxiedCursor;
|
|
4740
|
-
// If one of the advancing methods was not called, call continue().
|
|
4741
|
-
cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
|
|
4742
|
-
advanceResults.delete(proxiedCursor);
|
|
4743
|
-
}
|
|
4744
|
-
}
|
|
4745
|
-
function isIteratorProp(target, prop) {
|
|
4746
|
-
return ((prop === Symbol.asyncIterator &&
|
|
4747
|
-
instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor])) ||
|
|
4748
|
-
(prop === 'iterate' && instanceOfAny(target, [IDBIndex, IDBObjectStore])));
|
|
4749
|
-
}
|
|
4750
|
-
replaceTraps((oldTraps) => ({
|
|
4751
|
-
...oldTraps,
|
|
4752
|
-
get(target, prop, receiver) {
|
|
4753
|
-
if (isIteratorProp(target, prop))
|
|
4754
|
-
return iterate;
|
|
4755
|
-
return oldTraps.get(target, prop, receiver);
|
|
4756
|
-
},
|
|
4757
|
-
has(target, prop) {
|
|
4758
|
-
return isIteratorProp(target, prop) || oldTraps.has(target, prop);
|
|
4759
|
-
},
|
|
4760
|
-
}));
|
|
4761
|
-
|
|
4762
|
-
const log$d = createPrefixedLogger('CACHE-WEB');
|
|
4763
|
-
/**
|
|
4764
|
-
* Web cache adapter using IndexedDB with automatic error recovery
|
|
4765
|
-
* Cache never expires - data persists until explicitly invalidated
|
|
4766
|
-
*/
|
|
4767
|
-
class WebCacheAdapter {
|
|
4768
|
-
constructor(options = {}) {
|
|
4769
|
-
this.db = null;
|
|
4770
|
-
this.initPromise = null;
|
|
4771
|
-
this.retryCount = 0;
|
|
4772
|
-
this.maxRetries = 3;
|
|
4773
|
-
this.options = {
|
|
4774
|
-
maxSize: 50 * 1024 * 1024, // 50MB
|
|
4775
|
-
maxEntries: 10000,
|
|
4776
|
-
compression: false,
|
|
4777
|
-
compressionThreshold: 1024,
|
|
4778
|
-
...options,
|
|
4779
|
-
};
|
|
4780
|
-
this.initPromise = this.initialize();
|
|
4781
|
-
}
|
|
4782
|
-
async initialize() {
|
|
4783
|
-
if (this.db)
|
|
4784
|
-
return;
|
|
4785
|
-
log$d.debug('Initializing IndexedDB cache', {
|
|
4786
|
-
dbName: WebCacheAdapter.DB_NAME,
|
|
4787
|
-
version: WebCacheAdapter.DB_VERSION,
|
|
4788
|
-
});
|
|
4789
|
-
try {
|
|
4790
|
-
this.db = await this.openDatabase();
|
|
4791
|
-
log$d.debug('IndexedDB cache initialized successfully');
|
|
4792
|
-
this.retryCount = 0; // Reset retry count on success
|
|
4793
|
-
}
|
|
4794
|
-
catch (error) {
|
|
4795
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
4796
|
-
log$d.debug('Failed to initialize IndexedDB', { error: errorMessage });
|
|
4797
|
-
// Check if this is a version conflict error
|
|
4798
|
-
if (this.isVersionConflictError(errorMessage)) {
|
|
4799
|
-
await this.handleVersionConflict();
|
|
4800
|
-
}
|
|
4801
|
-
else {
|
|
4802
|
-
throw new Error(`Failed to initialize IndexedDB: ${errorMessage}`);
|
|
4803
|
-
}
|
|
4804
|
-
}
|
|
4805
|
-
}
|
|
4806
|
-
async openDatabase() {
|
|
4807
|
-
return await openDB(WebCacheAdapter.DB_NAME, WebCacheAdapter.DB_VERSION, {
|
|
4808
|
-
upgrade: (db, oldVersion, newVersion, transaction) => {
|
|
4809
|
-
log$d.debug('Database upgrade needed', { oldVersion, newVersion });
|
|
4810
|
-
this.handleUpgrade(db, oldVersion, newVersion, transaction);
|
|
4811
|
-
},
|
|
4812
|
-
blocked: () => {
|
|
4813
|
-
log$d.debug('Database blocked - another tab may be open');
|
|
4814
|
-
},
|
|
4815
|
-
blocking: () => {
|
|
4816
|
-
log$d.debug('Database blocking - will close connection');
|
|
4817
|
-
if (this.db) {
|
|
4818
|
-
this.db.close();
|
|
4819
|
-
this.db = null;
|
|
4820
|
-
}
|
|
4821
|
-
},
|
|
4822
|
-
terminated: () => {
|
|
4823
|
-
log$d.debug('Database connection terminated unexpectedly');
|
|
4824
|
-
this.db = null;
|
|
4825
|
-
},
|
|
4826
|
-
});
|
|
4827
|
-
}
|
|
4828
|
-
handleUpgrade(db, oldVersion, newVersion, transaction) {
|
|
4829
|
-
log$d.debug('Handling database upgrade', { oldVersion, newVersion });
|
|
4830
|
-
// Create cache store if it doesn't exist (initial setup)
|
|
4831
|
-
if (!db.objectStoreNames.contains(WebCacheAdapter.STORE_NAME)) {
|
|
4832
|
-
const store = db.createObjectStore(WebCacheAdapter.STORE_NAME, { keyPath: 'key' });
|
|
4833
|
-
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
4834
|
-
log$d.debug('Created cache store and timestamp index');
|
|
4835
|
-
}
|
|
4836
|
-
// Handle migration from version 1 to 2
|
|
4837
|
-
if (oldVersion < 2) {
|
|
4838
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
4839
|
-
// Remove unused indexes from simplified cache structure
|
|
4840
|
-
const indexesToRemove = ['tags', 'source', 'syncStatus'];
|
|
4841
|
-
indexesToRemove.forEach((indexName) => {
|
|
4842
|
-
try {
|
|
4843
|
-
if (store.indexNames.contains(indexName)) {
|
|
4844
|
-
store.deleteIndex(indexName);
|
|
4845
|
-
log$d.debug(`Removed unused index: ${indexName}`);
|
|
4846
|
-
}
|
|
4847
|
-
}
|
|
4848
|
-
catch (error) {
|
|
4849
|
-
// Ignore errors if indexes don't exist
|
|
4850
|
-
log$d.debug(`Warning: Could not remove index ${indexName}`, error);
|
|
4851
|
-
}
|
|
4852
|
-
});
|
|
4853
|
-
}
|
|
4854
|
-
log$d.debug('Database upgrade completed');
|
|
4855
|
-
}
|
|
4856
|
-
isVersionConflictError(errorMessage) {
|
|
4857
|
-
return (errorMessage.includes('less than the existing version') ||
|
|
4858
|
-
errorMessage.includes('version conflict') ||
|
|
4859
|
-
errorMessage.includes('VersionError'));
|
|
4860
|
-
}
|
|
4861
|
-
async handleVersionConflict() {
|
|
4862
|
-
log$d.debug('Handling version conflict, attempting recovery...');
|
|
4863
|
-
if (this.retryCount >= this.maxRetries) {
|
|
4864
|
-
throw new Error('Failed to resolve IndexedDB version conflict after multiple attempts');
|
|
4865
|
-
}
|
|
4866
|
-
this.retryCount++;
|
|
4867
|
-
try {
|
|
4868
|
-
// Close any existing connection
|
|
4869
|
-
if (this.db) {
|
|
4870
|
-
this.db.close();
|
|
4871
|
-
this.db = null;
|
|
4872
|
-
}
|
|
4873
|
-
// Delete the problematic database
|
|
4874
|
-
log$d.debug('Deleting problematic database to resolve version conflict');
|
|
4875
|
-
await deleteDB(WebCacheAdapter.DB_NAME);
|
|
4876
|
-
// Wait a bit for the deletion to complete
|
|
4877
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
4878
|
-
// Try to open the database again
|
|
4879
|
-
log$d.debug(`Retrying database initialization (attempt ${this.retryCount}/${this.maxRetries})`);
|
|
4880
|
-
this.db = await this.openDatabase();
|
|
4881
|
-
log$d.debug('Successfully recovered from version conflict');
|
|
4882
|
-
this.retryCount = 0; // Reset retry count on success
|
|
4883
|
-
}
|
|
4884
|
-
catch (retryError) {
|
|
4885
|
-
const retryErrorMessage = retryError instanceof Error ? retryError.message : 'Unknown error';
|
|
4886
|
-
log$d.debug('Recovery attempt failed', { attempt: this.retryCount, error: retryErrorMessage });
|
|
4887
|
-
if (this.retryCount < this.maxRetries) {
|
|
4888
|
-
// Try again
|
|
4889
|
-
await this.handleVersionConflict();
|
|
4890
|
-
}
|
|
4891
|
-
else {
|
|
4892
|
-
throw new Error(`Failed to recover from IndexedDB version conflict: ${retryErrorMessage}`);
|
|
4893
|
-
}
|
|
4894
|
-
}
|
|
4895
|
-
}
|
|
4896
|
-
async get(key) {
|
|
4897
|
-
await this.ensureInitialized();
|
|
4898
|
-
log$d.debug('Getting cache item', { key });
|
|
4899
|
-
try {
|
|
4900
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readonly');
|
|
4901
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
4902
|
-
const result = await store.get(key);
|
|
4903
|
-
if (!result) {
|
|
4904
|
-
return null;
|
|
4905
|
-
}
|
|
4906
|
-
const item = result;
|
|
4907
|
-
// Handle decompression if needed
|
|
4908
|
-
const isCompressed = !!item.compressed;
|
|
4909
|
-
let finalData;
|
|
4910
|
-
if (isCompressed) {
|
|
4911
|
-
const decompressed = decompressData(item.data, true);
|
|
4912
|
-
finalData = JSON.parse(decompressed.data);
|
|
4913
|
-
}
|
|
4914
|
-
else {
|
|
4915
|
-
finalData = item.data;
|
|
4916
|
-
}
|
|
4917
|
-
return {
|
|
4918
|
-
data: finalData,
|
|
4919
|
-
timestamp: item.timestamp,
|
|
4920
|
-
compressed: isCompressed,
|
|
4921
|
-
};
|
|
4922
|
-
}
|
|
4923
|
-
catch (error) {
|
|
4924
|
-
log$d.debug('Error getting cache item', { key, error });
|
|
4925
|
-
return null; // Return null on error instead of throwing
|
|
4926
|
-
}
|
|
4927
|
-
}
|
|
4928
|
-
async set(key, data) {
|
|
4929
|
-
const item = {
|
|
4930
|
-
data,
|
|
4931
|
-
timestamp: Date.now(),
|
|
4932
|
-
};
|
|
4933
|
-
return this.setItem(key, item);
|
|
4934
|
-
}
|
|
4935
|
-
async setItem(key, item) {
|
|
4936
|
-
await this.ensureInitialized();
|
|
4937
|
-
// Handle compression if enabled
|
|
4938
|
-
let finalData = item.data;
|
|
4939
|
-
let isCompressed = false;
|
|
4940
|
-
if (this.options.compression && this.options.compressionThreshold) {
|
|
4941
|
-
const serializedData = JSON.stringify(item.data);
|
|
4942
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4943
|
-
if (compressionResult.compressed) {
|
|
4944
|
-
finalData = compressionResult.data;
|
|
4945
|
-
isCompressed = true;
|
|
4946
|
-
log$d.debug('Compression result', {
|
|
4947
|
-
key,
|
|
4948
|
-
originalSize: compressionResult.originalSize,
|
|
4949
|
-
compressedSize: compressionResult.compressedSize,
|
|
4950
|
-
compressed: isCompressed,
|
|
4951
|
-
savings: compressionResult.originalSize - compressionResult.compressedSize,
|
|
4952
|
-
});
|
|
4953
|
-
}
|
|
4954
|
-
}
|
|
4955
|
-
log$d.debug('Setting cache item', { key, timestamp: item.timestamp, compressed: isCompressed });
|
|
4956
|
-
const storedItem = {
|
|
4957
|
-
key,
|
|
4958
|
-
data: finalData,
|
|
4959
|
-
timestamp: item.timestamp,
|
|
4960
|
-
compressed: isCompressed,
|
|
4961
|
-
};
|
|
4962
|
-
try {
|
|
4963
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readwrite');
|
|
4964
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
4965
|
-
await store.put(storedItem);
|
|
4966
|
-
}
|
|
4967
|
-
catch (error) {
|
|
4968
|
-
log$d.debug('Error setting cache item', { key, error });
|
|
4969
|
-
// Silently fail for cache writes
|
|
2756
|
+
message.includes('tls') ||
|
|
2757
|
+
message.includes('handshake'));
|
|
4970
2758
|
}
|
|
2759
|
+
return false;
|
|
4971
2760
|
}
|
|
4972
|
-
async
|
|
4973
|
-
if (
|
|
4974
|
-
|
|
4975
|
-
await this.ensureInitialized();
|
|
4976
|
-
log$d.debug('Batch setting items', { count: items.length });
|
|
4977
|
-
try {
|
|
4978
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readwrite');
|
|
4979
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
4980
|
-
// Process all items in the transaction
|
|
4981
|
-
const promises = items.map(([key, item]) => {
|
|
4982
|
-
const storedItem = this.prepareBatchItem(key, item);
|
|
4983
|
-
return store.put(storedItem);
|
|
4984
|
-
});
|
|
4985
|
-
await Promise.all(promises);
|
|
4986
|
-
log$d.debug('Batch operation completed', { count: items.length });
|
|
4987
|
-
}
|
|
4988
|
-
catch (error) {
|
|
4989
|
-
log$d.debug('Error in batch operation', { count: items.length, error });
|
|
4990
|
-
// Silently fail for batch writes
|
|
4991
|
-
}
|
|
4992
|
-
}
|
|
4993
|
-
prepareBatchItem(key, item) {
|
|
4994
|
-
// Handle compression if enabled (same logic as setItem)
|
|
4995
|
-
let finalData = item.data;
|
|
4996
|
-
let isCompressed = false;
|
|
4997
|
-
if (this.options.compression && this.options.compressionThreshold) {
|
|
4998
|
-
const serializedData = JSON.stringify(item.data);
|
|
4999
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
5000
|
-
if (compressionResult.compressed) {
|
|
5001
|
-
finalData = compressionResult.data;
|
|
5002
|
-
isCompressed = true;
|
|
5003
|
-
}
|
|
2761
|
+
async configureCertificate(certificate) {
|
|
2762
|
+
if (!this.mtlsAdapter) {
|
|
2763
|
+
throw new Error('mTLS adapter not available');
|
|
5004
2764
|
}
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
compressed: isCompressed,
|
|
2765
|
+
const certificateData = {
|
|
2766
|
+
certificate: certificate.certificate,
|
|
2767
|
+
privateKey: certificate.privateKey,
|
|
2768
|
+
format: certificate.format.toUpperCase(),
|
|
5010
2769
|
};
|
|
2770
|
+
await this.mtlsAdapter.configureCertificate(certificateData);
|
|
5011
2771
|
}
|
|
5012
|
-
async
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
const deletePromises = keys.map((key) => this.delete(key));
|
|
5016
|
-
await Promise.all(deletePromises);
|
|
5017
|
-
}
|
|
5018
|
-
async clear() {
|
|
5019
|
-
await this.ensureInitialized();
|
|
5020
|
-
try {
|
|
5021
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readwrite');
|
|
5022
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
5023
|
-
await store.clear();
|
|
5024
|
-
log$d.debug('Cache cleared successfully');
|
|
5025
|
-
}
|
|
5026
|
-
catch (error) {
|
|
5027
|
-
log$d.debug('Error clearing cache', error);
|
|
5028
|
-
// Silently fail for cache clear
|
|
2772
|
+
async storeCertificate(certificate, privateKey, options = {}) {
|
|
2773
|
+
if (!this.certificatePort) {
|
|
2774
|
+
throw new Error('Certificate port not available');
|
|
5029
2775
|
}
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
let entries = 0;
|
|
5037
|
-
let bytes = 0;
|
|
5038
|
-
// Use cursor for efficient iteration
|
|
5039
|
-
let cursor = await store.openCursor();
|
|
5040
|
-
while (cursor) {
|
|
5041
|
-
entries++;
|
|
5042
|
-
// Rough estimation of size
|
|
5043
|
-
bytes += JSON.stringify(cursor.value).length * 2; // UTF-16 encoding
|
|
5044
|
-
cursor = await cursor.continue();
|
|
2776
|
+
if (this.mtlsAdapter) {
|
|
2777
|
+
try {
|
|
2778
|
+
await this.mtlsAdapter.removeCertificate();
|
|
2779
|
+
}
|
|
2780
|
+
catch {
|
|
2781
|
+
// No existing certificate to remove
|
|
5045
2782
|
}
|
|
5046
|
-
return {
|
|
5047
|
-
entries,
|
|
5048
|
-
bytes,
|
|
5049
|
-
lastCleanup: Date.now(),
|
|
5050
|
-
};
|
|
5051
2783
|
}
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
2784
|
+
const format = (options.format || 'pem');
|
|
2785
|
+
await this.certificatePort.storeCertificate(certificate, privateKey, format);
|
|
2786
|
+
if (this.mtlsAdapter) {
|
|
2787
|
+
const certificateData = {
|
|
2788
|
+
certificate,
|
|
2789
|
+
privateKey,
|
|
2790
|
+
format: format.toUpperCase(),
|
|
5058
2791
|
};
|
|
2792
|
+
await this.mtlsAdapter.configureCertificate(certificateData);
|
|
5059
2793
|
}
|
|
5060
2794
|
}
|
|
5061
|
-
async
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readonly');
|
|
5069
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
5070
|
-
const allKeys = (await store.getAllKeys());
|
|
5071
|
-
if (!pattern) {
|
|
5072
|
-
return allKeys;
|
|
2795
|
+
async clearCertificate() {
|
|
2796
|
+
if (this.mtlsAdapter) {
|
|
2797
|
+
try {
|
|
2798
|
+
await this.mtlsAdapter.removeCertificate();
|
|
2799
|
+
}
|
|
2800
|
+
catch {
|
|
2801
|
+
// No certificate to remove
|
|
5073
2802
|
}
|
|
5074
|
-
const regex = this.patternToRegex(pattern);
|
|
5075
|
-
return allKeys.filter((key) => regex.test(key));
|
|
5076
2803
|
}
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
return [];
|
|
2804
|
+
if (this.certificatePort) {
|
|
2805
|
+
await this.certificatePort.clearCertificate();
|
|
5080
2806
|
}
|
|
5081
2807
|
}
|
|
5082
|
-
async
|
|
5083
|
-
|
|
5084
|
-
try {
|
|
5085
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readwrite');
|
|
5086
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
5087
|
-
await store.delete(key);
|
|
5088
|
-
return true;
|
|
5089
|
-
}
|
|
5090
|
-
catch (error) {
|
|
5091
|
-
log$d.debug('Error deleting cache item', { key, error });
|
|
2808
|
+
async testConnection() {
|
|
2809
|
+
if (!this.mtlsAdapter) {
|
|
5092
2810
|
return false;
|
|
5093
2811
|
}
|
|
2812
|
+
return this.mtlsAdapter.testConnection();
|
|
5094
2813
|
}
|
|
5095
|
-
|
|
5096
|
-
if (!this.
|
|
5097
|
-
|
|
5098
|
-
}
|
|
5099
|
-
try {
|
|
5100
|
-
await this.initPromise;
|
|
5101
|
-
}
|
|
5102
|
-
catch (error) {
|
|
5103
|
-
log$d.debug('Failed to ensure initialization', error);
|
|
5104
|
-
// Reset and try once more
|
|
5105
|
-
this.initPromise = null;
|
|
5106
|
-
this.db = null;
|
|
5107
|
-
this.initPromise = this.initialize();
|
|
5108
|
-
await this.initPromise;
|
|
2814
|
+
getBaseUrl() {
|
|
2815
|
+
if (!this.mtlsAdapter) {
|
|
2816
|
+
return null;
|
|
5109
2817
|
}
|
|
2818
|
+
return this.mtlsAdapter.getBaseUrl();
|
|
5110
2819
|
}
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
case 'web':
|
|
5127
|
-
return new WebCacheAdapter({
|
|
5128
|
-
maxSize: 50 * 1024 * 1024,
|
|
5129
|
-
maxEntries: 10000,
|
|
5130
|
-
compression: false,
|
|
5131
|
-
});
|
|
5132
|
-
case 'react-native':
|
|
5133
|
-
try {
|
|
5134
|
-
return new ReactNativeCacheAdapter({
|
|
5135
|
-
maxSize: 100 * 1024 * 1024,
|
|
5136
|
-
maxEntries: 15000,
|
|
5137
|
-
});
|
|
5138
|
-
}
|
|
5139
|
-
catch {
|
|
5140
|
-
return new MemoryCacheAdapter({
|
|
5141
|
-
maxSize: 10 * 1024 * 1024,
|
|
5142
|
-
maxEntries: 5000,
|
|
5143
|
-
});
|
|
2820
|
+
async getStatus() {
|
|
2821
|
+
const status = {
|
|
2822
|
+
adapterAvailable: !!this.mtlsAdapter,
|
|
2823
|
+
certificatePortAvailable: !!this.certificatePort,
|
|
2824
|
+
isReady: false,
|
|
2825
|
+
hasCertificate: false,
|
|
2826
|
+
certificateInfo: null,
|
|
2827
|
+
platformInfo: this.mtlsAdapter?.getPlatformInfo() || null,
|
|
2828
|
+
pendingRequestsCount: this.pendingRequests.size,
|
|
2829
|
+
};
|
|
2830
|
+
if (this.certificatePort) {
|
|
2831
|
+
try {
|
|
2832
|
+
status.hasCertificate = await this.certificatePort.hasCertificate();
|
|
2833
|
+
if (status.hasCertificate) {
|
|
2834
|
+
status.certificateInfo = await this.certificatePort.getCertificateInfo();
|
|
5144
2835
|
}
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
maxEntries: 5000,
|
|
5150
|
-
});
|
|
2836
|
+
}
|
|
2837
|
+
catch {
|
|
2838
|
+
// Ignore errors
|
|
2839
|
+
}
|
|
5151
2840
|
}
|
|
2841
|
+
status.isReady = await this.isMtlsReady();
|
|
2842
|
+
return status;
|
|
5152
2843
|
}
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
return undefined;
|
|
2844
|
+
clearPendingRequests() {
|
|
2845
|
+
this.pendingRequests.clear();
|
|
5156
2846
|
}
|
|
5157
2847
|
}
|
|
5158
2848
|
|
|
5159
|
-
/**
|
|
5160
|
-
* Mixin that adds multiGet, multiSet, multiRemove to any storage adapter
|
|
5161
|
-
* Eliminates duplicate code across Node, Web, and React Native adapters
|
|
5162
|
-
*/
|
|
5163
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5164
2849
|
/**
|
|
5165
2850
|
* Abstract base class for standard storage adapters
|
|
5166
2851
|
* Implements multi-operations, subclasses implement core operations
|
|
@@ -5208,7 +2893,7 @@ class BaseSecureStorageAdapter {
|
|
|
5208
2893
|
}
|
|
5209
2894
|
}
|
|
5210
2895
|
|
|
5211
|
-
const log$
|
|
2896
|
+
const log$8 = createPrefixedLogger('NETWORK-BASE');
|
|
5212
2897
|
class NetworkBase {
|
|
5213
2898
|
constructor(initialOnline = true, debounceMs = 300) {
|
|
5214
2899
|
this.destroy$ = new Subject();
|
|
@@ -5228,14 +2913,14 @@ class NetworkBase {
|
|
|
5228
2913
|
const current = this.statusSubject.getValue();
|
|
5229
2914
|
if (current.online !== online) {
|
|
5230
2915
|
this.statusSubject.next({ online, timestamp: Date.now() });
|
|
5231
|
-
log$
|
|
2916
|
+
log$8.debug(`Network status changed: ${online ? 'online' : 'offline'}`);
|
|
5232
2917
|
}
|
|
5233
2918
|
}
|
|
5234
2919
|
destroy() {
|
|
5235
2920
|
this.destroy$.next();
|
|
5236
2921
|
this.destroy$.complete();
|
|
5237
2922
|
this.statusSubject.complete();
|
|
5238
|
-
log$
|
|
2923
|
+
log$8.debug('Network monitor destroyed');
|
|
5239
2924
|
}
|
|
5240
2925
|
}
|
|
5241
2926
|
|
|
@@ -5295,7 +2980,7 @@ class NodeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5295
2980
|
}
|
|
5296
2981
|
}
|
|
5297
2982
|
|
|
5298
|
-
const log$
|
|
2983
|
+
const log$7 = createPrefixedLogger('RN-STORAGE');
|
|
5299
2984
|
/**
|
|
5300
2985
|
* React Native storage adapter using AsyncStorage
|
|
5301
2986
|
* Note: Uses native batch operations for better performance (not base class)
|
|
@@ -5327,7 +3012,7 @@ class ReactNativeStorageAdapter {
|
|
|
5327
3012
|
return await this.AsyncStorage.getItem(key);
|
|
5328
3013
|
}
|
|
5329
3014
|
catch (error) {
|
|
5330
|
-
log$
|
|
3015
|
+
log$7.error('Failed to get item from AsyncStorage:', error);
|
|
5331
3016
|
return null;
|
|
5332
3017
|
}
|
|
5333
3018
|
}
|
|
@@ -5368,7 +3053,7 @@ class ReactNativeStorageAdapter {
|
|
|
5368
3053
|
return await this.AsyncStorage.getAllKeys();
|
|
5369
3054
|
}
|
|
5370
3055
|
catch (error) {
|
|
5371
|
-
log$
|
|
3056
|
+
log$7.error('Failed to get all keys:', error);
|
|
5372
3057
|
return [];
|
|
5373
3058
|
}
|
|
5374
3059
|
}
|
|
@@ -5385,7 +3070,7 @@ class ReactNativeStorageAdapter {
|
|
|
5385
3070
|
return result;
|
|
5386
3071
|
}
|
|
5387
3072
|
catch (error) {
|
|
5388
|
-
log$
|
|
3073
|
+
log$7.error('Failed to get multiple items:', error);
|
|
5389
3074
|
const result = {};
|
|
5390
3075
|
keys.forEach((key) => {
|
|
5391
3076
|
result[key] = null;
|
|
@@ -5435,7 +3120,7 @@ class ReactNativeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5435
3120
|
return;
|
|
5436
3121
|
}
|
|
5437
3122
|
catch {
|
|
5438
|
-
log$
|
|
3123
|
+
log$7.debug('expo-secure-store not available, trying react-native-keychain');
|
|
5439
3124
|
}
|
|
5440
3125
|
try {
|
|
5441
3126
|
const Keychain = require('react-native-keychain');
|
|
@@ -5444,7 +3129,7 @@ class ReactNativeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5444
3129
|
return;
|
|
5445
3130
|
}
|
|
5446
3131
|
catch {
|
|
5447
|
-
log$
|
|
3132
|
+
log$7.error('react-native-keychain not available');
|
|
5448
3133
|
}
|
|
5449
3134
|
throw new Error('No secure storage available. Please install expo-secure-store or react-native-keychain');
|
|
5450
3135
|
}
|
|
@@ -5462,7 +3147,7 @@ class ReactNativeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5462
3147
|
}
|
|
5463
3148
|
}
|
|
5464
3149
|
catch (error) {
|
|
5465
|
-
log$
|
|
3150
|
+
log$7.error('Failed to get secure item:', error);
|
|
5466
3151
|
}
|
|
5467
3152
|
return null;
|
|
5468
3153
|
}
|
|
@@ -5499,10 +3184,10 @@ class ReactNativeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5499
3184
|
}
|
|
5500
3185
|
}
|
|
5501
3186
|
async clear() {
|
|
5502
|
-
log$
|
|
3187
|
+
log$7.warn('Clear all secure items not fully implemented for React Native');
|
|
5503
3188
|
}
|
|
5504
3189
|
async getAllKeys() {
|
|
5505
|
-
log$
|
|
3190
|
+
log$7.warn('Get all secure keys not implemented for React Native');
|
|
5506
3191
|
return [];
|
|
5507
3192
|
}
|
|
5508
3193
|
async isAvailable() {
|
|
@@ -5720,7 +3405,7 @@ class NodeNetworkMonitor extends NetworkBase {
|
|
|
5720
3405
|
}
|
|
5721
3406
|
}
|
|
5722
3407
|
|
|
5723
|
-
const log$
|
|
3408
|
+
const log$6 = createPrefixedLogger('RN-NETWORK');
|
|
5724
3409
|
/**
|
|
5725
3410
|
* React Native network monitor using RxJS
|
|
5726
3411
|
* Supports both @react-native-community/netinfo and expo-network
|
|
@@ -5733,7 +3418,7 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5733
3418
|
this.moduleReady$ = new Subject();
|
|
5734
3419
|
this.isExpo = detectPlatform().isExpo;
|
|
5735
3420
|
this.init().catch((error) => {
|
|
5736
|
-
log$
|
|
3421
|
+
log$6.error('Network monitor initialization failed:', error);
|
|
5737
3422
|
});
|
|
5738
3423
|
}
|
|
5739
3424
|
async init() {
|
|
@@ -5752,10 +3437,10 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5752
3437
|
try {
|
|
5753
3438
|
const module = require('@react-native-community/netinfo');
|
|
5754
3439
|
this.netInfoModule = module.default || module;
|
|
5755
|
-
log$
|
|
3440
|
+
log$6.debug('Loaded @react-native-community/netinfo module');
|
|
5756
3441
|
}
|
|
5757
3442
|
catch (error) {
|
|
5758
|
-
log$
|
|
3443
|
+
log$6.error('Failed to load React Native NetInfo module:', error);
|
|
5759
3444
|
this.netInfoModule = null;
|
|
5760
3445
|
}
|
|
5761
3446
|
}
|
|
@@ -5763,10 +3448,10 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5763
3448
|
try {
|
|
5764
3449
|
const module = require('expo-network');
|
|
5765
3450
|
this.netInfoModule = module.default || module;
|
|
5766
|
-
log$
|
|
3451
|
+
log$6.debug('Loaded expo-network module');
|
|
5767
3452
|
}
|
|
5768
3453
|
catch (error) {
|
|
5769
|
-
log$
|
|
3454
|
+
log$6.error('Failed to load Expo Network module:', error);
|
|
5770
3455
|
this.netInfoModule = null;
|
|
5771
3456
|
}
|
|
5772
3457
|
}
|
|
@@ -5783,16 +3468,16 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5783
3468
|
}
|
|
5784
3469
|
const online = !!(state.isConnected && state.isInternetReachable !== false);
|
|
5785
3470
|
this.updateStatus(online);
|
|
5786
|
-
log$
|
|
3471
|
+
log$6.debug('Initial network state:', online ? 'online' : 'offline');
|
|
5787
3472
|
}
|
|
5788
3473
|
catch (error) {
|
|
5789
|
-
log$
|
|
3474
|
+
log$6.warn('Could not fetch initial network state:', error);
|
|
5790
3475
|
}
|
|
5791
3476
|
}
|
|
5792
3477
|
subscribeToStateChanges() {
|
|
5793
3478
|
if (!this.netInfoModule)
|
|
5794
3479
|
return;
|
|
5795
|
-
log$
|
|
3480
|
+
log$6.debug('Subscribing to network state changes');
|
|
5796
3481
|
const handleState = (state) => {
|
|
5797
3482
|
const online = !!(state.isConnected && (state.isInternetReachable ?? true));
|
|
5798
3483
|
this.updateStatus(online);
|
|
@@ -5834,7 +3519,7 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5834
3519
|
};
|
|
5835
3520
|
}
|
|
5836
3521
|
catch (error) {
|
|
5837
|
-
log$
|
|
3522
|
+
log$6.error('Failed to fetch detailed network info:', error);
|
|
5838
3523
|
return null;
|
|
5839
3524
|
}
|
|
5840
3525
|
}
|
|
@@ -5934,7 +3619,7 @@ function loadNetworkMonitor(platform) {
|
|
|
5934
3619
|
}
|
|
5935
3620
|
}
|
|
5936
3621
|
|
|
5937
|
-
var MTLSErrorType
|
|
3622
|
+
var MTLSErrorType;
|
|
5938
3623
|
(function (MTLSErrorType) {
|
|
5939
3624
|
MTLSErrorType["NOT_SUPPORTED"] = "MTLS_NOT_SUPPORTED";
|
|
5940
3625
|
MTLSErrorType["CERTIFICATE_NOT_FOUND"] = "MTLS_CERTIFICATE_NOT_FOUND";
|
|
@@ -5943,7 +3628,7 @@ var MTLSErrorType$1;
|
|
|
5943
3628
|
MTLSErrorType["CONNECTION_FAILED"] = "MTLS_CONNECTION_FAILED";
|
|
5944
3629
|
MTLSErrorType["AUTHENTICATION_FAILED"] = "MTLS_AUTHENTICATION_FAILED";
|
|
5945
3630
|
MTLSErrorType["CONFIGURATION_ERROR"] = "MTLS_CONFIGURATION_ERROR";
|
|
5946
|
-
})(MTLSErrorType
|
|
3631
|
+
})(MTLSErrorType || (MTLSErrorType = {}));
|
|
5947
3632
|
class MTLSError extends Error {
|
|
5948
3633
|
constructor(type, message, originalError, statusCode) {
|
|
5949
3634
|
super(message);
|
|
@@ -5970,7 +3655,7 @@ class CertificateValidator {
|
|
|
5970
3655
|
}
|
|
5971
3656
|
}
|
|
5972
3657
|
|
|
5973
|
-
const log$
|
|
3658
|
+
const log$5 = createPrefixedLogger('RN-MTLS');
|
|
5974
3659
|
/**
|
|
5975
3660
|
* React Native mTLS Adapter using @a-cube-io/expo-mutual-tls
|
|
5976
3661
|
*/
|
|
@@ -5992,7 +3677,7 @@ class ReactNativeMTLSAdapter {
|
|
|
5992
3677
|
this.expoMTLS = ExpoMutualTls;
|
|
5993
3678
|
// Set up debug logging with the correct event signature
|
|
5994
3679
|
const debugListener = ExpoMutualTls.onDebugLog((event) => {
|
|
5995
|
-
log$
|
|
3680
|
+
log$5.debug(`${event.type}: ${event.message}`, {
|
|
5996
3681
|
method: event.method,
|
|
5997
3682
|
url: event.url,
|
|
5998
3683
|
statusCode: event.statusCode,
|
|
@@ -6002,28 +3687,28 @@ class ReactNativeMTLSAdapter {
|
|
|
6002
3687
|
this.eventListeners.push(debugListener);
|
|
6003
3688
|
// Set up error logging with the correct event signature
|
|
6004
3689
|
const errorListener = ExpoMutualTls.onError((event) => {
|
|
6005
|
-
log$
|
|
3690
|
+
log$5.error(event.message, {
|
|
6006
3691
|
code: event.code,
|
|
6007
3692
|
});
|
|
6008
3693
|
});
|
|
6009
3694
|
this.eventListeners.push(errorListener);
|
|
6010
3695
|
// Set up certificate expiry monitoring with the correct event signature
|
|
6011
3696
|
const expiryListener = ExpoMutualTls.onCertificateExpiry((event) => {
|
|
6012
|
-
log$
|
|
3697
|
+
log$5.warn(`Certificate ${event.subject} expires at ${new Date(event.expiry)}`, {
|
|
6013
3698
|
alias: event.alias,
|
|
6014
3699
|
warning: event.warning,
|
|
6015
3700
|
});
|
|
6016
3701
|
});
|
|
6017
3702
|
this.eventListeners.push(expiryListener);
|
|
6018
|
-
log$
|
|
3703
|
+
log$5.debug('Expo mTLS module loaded successfully');
|
|
6019
3704
|
}
|
|
6020
3705
|
catch (error) {
|
|
6021
|
-
log$
|
|
3706
|
+
log$5.warn('@a-cube-io/expo-mutual-tls not available:', error);
|
|
6022
3707
|
}
|
|
6023
3708
|
}
|
|
6024
3709
|
async isMTLSSupported() {
|
|
6025
3710
|
const supported = this.expoMTLS !== null;
|
|
6026
|
-
log$
|
|
3711
|
+
log$5.debug('mTLS support check:', {
|
|
6027
3712
|
supported,
|
|
6028
3713
|
platform: this.getPlatformInfo().platform,
|
|
6029
3714
|
moduleAvailable: !!this.expoMTLS,
|
|
@@ -6032,10 +3717,10 @@ class ReactNativeMTLSAdapter {
|
|
|
6032
3717
|
}
|
|
6033
3718
|
async initialize(config) {
|
|
6034
3719
|
if (!this.expoMTLS) {
|
|
6035
|
-
throw new MTLSError(MTLSErrorType
|
|
3720
|
+
throw new MTLSError(MTLSErrorType.NOT_SUPPORTED, 'Expo mTLS module not available');
|
|
6036
3721
|
}
|
|
6037
3722
|
this.config = config;
|
|
6038
|
-
log$
|
|
3723
|
+
log$5.debug('Initialized with config:', {
|
|
6039
3724
|
baseUrl: config.baseUrl,
|
|
6040
3725
|
port: config.port,
|
|
6041
3726
|
timeout: config.timeout,
|
|
@@ -6044,12 +3729,12 @@ class ReactNativeMTLSAdapter {
|
|
|
6044
3729
|
}
|
|
6045
3730
|
async configureCertificate(certificateData) {
|
|
6046
3731
|
if (!this.expoMTLS) {
|
|
6047
|
-
throw new MTLSError(MTLSErrorType
|
|
3732
|
+
throw new MTLSError(MTLSErrorType.NOT_SUPPORTED, 'Expo mTLS module not available');
|
|
6048
3733
|
}
|
|
6049
3734
|
if (!this.config) {
|
|
6050
|
-
throw new MTLSError(MTLSErrorType
|
|
3735
|
+
throw new MTLSError(MTLSErrorType.CONFIGURATION_ERROR, 'Adapter not initialized. Call initialize() first.');
|
|
6051
3736
|
}
|
|
6052
|
-
log$
|
|
3737
|
+
log$5.debug('Configuring certificate:', {
|
|
6053
3738
|
format: certificateData.format,
|
|
6054
3739
|
hasPassword: !!certificateData.password,
|
|
6055
3740
|
certificateLength: certificateData.certificate.length,
|
|
@@ -6059,47 +3744,47 @@ class ReactNativeMTLSAdapter {
|
|
|
6059
3744
|
if (certificateData.format === 'PEM') {
|
|
6060
3745
|
// Validate PEM format
|
|
6061
3746
|
if (!CertificateValidator.validatePEMFormat(certificateData.certificate, certificateData.privateKey)) {
|
|
6062
|
-
throw new MTLSError(MTLSErrorType
|
|
3747
|
+
throw new MTLSError(MTLSErrorType.CERTIFICATE_INVALID, 'Invalid PEM certificate format');
|
|
6063
3748
|
}
|
|
6064
3749
|
// Step 1: Configure PEM services (optional parameters for keychain services)
|
|
6065
3750
|
const configResult = await this.expoMTLS.configurePEM('client-cert-service', // certService
|
|
6066
3751
|
'client-key-service', // keyService
|
|
6067
3752
|
true // enableLogging - let the native module handle its own debug logging
|
|
6068
3753
|
);
|
|
6069
|
-
log$
|
|
3754
|
+
log$5.debug('PEM services configured:', configResult);
|
|
6070
3755
|
if (!configResult.success) {
|
|
6071
|
-
throw new MTLSError(MTLSErrorType
|
|
3756
|
+
throw new MTLSError(MTLSErrorType.CONFIGURATION_ERROR, `PEM configuration failed: ${configResult.state}`);
|
|
6072
3757
|
}
|
|
6073
3758
|
// Step 2: Store the actual PEM certificate and private key
|
|
6074
3759
|
const storeResult = await this.expoMTLS.storePEM(certificateData.certificate, certificateData.privateKey, certificateData.password // passphrase (optional)
|
|
6075
3760
|
);
|
|
6076
|
-
log$
|
|
3761
|
+
log$5.debug('PEM certificate store result:', storeResult);
|
|
6077
3762
|
if (!storeResult) {
|
|
6078
|
-
throw new MTLSError(MTLSErrorType
|
|
3763
|
+
throw new MTLSError(MTLSErrorType.CERTIFICATE_INVALID, 'Failed to store PEM certificate');
|
|
6079
3764
|
}
|
|
6080
3765
|
}
|
|
6081
3766
|
else if (certificateData.format === 'P12') {
|
|
6082
3767
|
if (!certificateData.password) {
|
|
6083
|
-
throw new MTLSError(MTLSErrorType
|
|
3768
|
+
throw new MTLSError(MTLSErrorType.CONFIGURATION_ERROR, 'P12 certificate requires password');
|
|
6084
3769
|
}
|
|
6085
3770
|
// Step 1: Configure P12 keychain service
|
|
6086
3771
|
const configResult = await this.expoMTLS.configureP12('client-p12-service', // keychainService
|
|
6087
3772
|
true // enableLogging - let the native module handle its own debug logging
|
|
6088
3773
|
);
|
|
6089
|
-
log$
|
|
3774
|
+
log$5.debug('P12 service configured:', configResult);
|
|
6090
3775
|
if (!configResult.success) {
|
|
6091
|
-
throw new MTLSError(MTLSErrorType
|
|
3776
|
+
throw new MTLSError(MTLSErrorType.CONFIGURATION_ERROR, `P12 configuration failed: ${configResult.state}`);
|
|
6092
3777
|
}
|
|
6093
3778
|
// Step 2: Store the P12 certificate data
|
|
6094
3779
|
const storeResult = await this.expoMTLS.storeP12(certificateData.certificate, // P12 data in certificate field
|
|
6095
3780
|
certificateData.password);
|
|
6096
|
-
log$
|
|
3781
|
+
log$5.debug('P12 certificate store result:', storeResult);
|
|
6097
3782
|
if (!storeResult) {
|
|
6098
|
-
throw new MTLSError(MTLSErrorType
|
|
3783
|
+
throw new MTLSError(MTLSErrorType.CERTIFICATE_INVALID, 'Failed to store P12 certificate');
|
|
6099
3784
|
}
|
|
6100
3785
|
}
|
|
6101
3786
|
else {
|
|
6102
|
-
throw new MTLSError(MTLSErrorType
|
|
3787
|
+
throw new MTLSError(MTLSErrorType.CERTIFICATE_INVALID, `Unsupported certificate format: ${certificateData.format}`);
|
|
6103
3788
|
}
|
|
6104
3789
|
this.isConfigured = true;
|
|
6105
3790
|
}
|
|
@@ -6107,8 +3792,8 @@ class ReactNativeMTLSAdapter {
|
|
|
6107
3792
|
if (error instanceof MTLSError) {
|
|
6108
3793
|
throw error;
|
|
6109
3794
|
}
|
|
6110
|
-
log$
|
|
6111
|
-
throw new MTLSError(MTLSErrorType
|
|
3795
|
+
log$5.error('Certificate configuration failed:', error);
|
|
3796
|
+
throw new MTLSError(MTLSErrorType.CONFIGURATION_ERROR, 'Failed to configure certificate', error);
|
|
6112
3797
|
}
|
|
6113
3798
|
}
|
|
6114
3799
|
async hasCertificate() {
|
|
@@ -6118,38 +3803,38 @@ class ReactNativeMTLSAdapter {
|
|
|
6118
3803
|
try {
|
|
6119
3804
|
// Use static method call
|
|
6120
3805
|
const hasCert = await this.expoMTLS.hasCertificate();
|
|
6121
|
-
log$
|
|
3806
|
+
log$5.debug('Certificate availability check:', hasCert);
|
|
6122
3807
|
return hasCert;
|
|
6123
3808
|
}
|
|
6124
3809
|
catch (error) {
|
|
6125
|
-
log$
|
|
3810
|
+
log$5.error('Certificate check failed:', error);
|
|
6126
3811
|
return false;
|
|
6127
3812
|
}
|
|
6128
3813
|
}
|
|
6129
3814
|
async getCertificateInfo() {
|
|
6130
3815
|
if (!this.expoMTLS) {
|
|
6131
|
-
log$
|
|
3816
|
+
log$5.debug('Certificate info requested but module not available');
|
|
6132
3817
|
return null;
|
|
6133
3818
|
}
|
|
6134
3819
|
try {
|
|
6135
3820
|
const hasCert = await this.hasCertificate();
|
|
6136
3821
|
if (!hasCert) {
|
|
6137
|
-
log$
|
|
3822
|
+
log$5.debug('No certificate stored');
|
|
6138
3823
|
return null;
|
|
6139
3824
|
}
|
|
6140
3825
|
// Use getCertificatesInfo to retrieve information about stored certificates
|
|
6141
3826
|
const result = await this.expoMTLS.getCertificatesInfo();
|
|
6142
3827
|
if (!result || !result.certificates || result.certificates.length === 0) {
|
|
6143
|
-
log$
|
|
3828
|
+
log$5.debug('No certificate information available');
|
|
6144
3829
|
return null;
|
|
6145
3830
|
}
|
|
6146
3831
|
// Get the first certificate (primary client certificate)
|
|
6147
3832
|
const cert = result.certificates[0];
|
|
6148
3833
|
if (!cert) {
|
|
6149
|
-
log$
|
|
3834
|
+
log$5.debug('Certificate data is empty');
|
|
6150
3835
|
return null;
|
|
6151
3836
|
}
|
|
6152
|
-
log$
|
|
3837
|
+
log$5.debug('Retrieved certificate info:', {
|
|
6153
3838
|
subject: cert.subject.commonName,
|
|
6154
3839
|
issuer: cert.issuer.commonName,
|
|
6155
3840
|
validFrom: new Date(cert.validFrom),
|
|
@@ -6168,7 +3853,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6168
3853
|
};
|
|
6169
3854
|
}
|
|
6170
3855
|
catch (error) {
|
|
6171
|
-
log$
|
|
3856
|
+
log$5.error('Failed to get certificate info:', error);
|
|
6172
3857
|
return null;
|
|
6173
3858
|
}
|
|
6174
3859
|
}
|
|
@@ -6179,14 +3864,14 @@ class ReactNativeMTLSAdapter {
|
|
|
6179
3864
|
*/
|
|
6180
3865
|
async parseCertificateData(certificateData) {
|
|
6181
3866
|
if (!this.expoMTLS) {
|
|
6182
|
-
log$
|
|
3867
|
+
log$5.debug('Parse certificate: Module not available');
|
|
6183
3868
|
return null;
|
|
6184
3869
|
}
|
|
6185
3870
|
try {
|
|
6186
3871
|
let result;
|
|
6187
3872
|
if (certificateData.format === 'P12') {
|
|
6188
3873
|
if (!certificateData.password) {
|
|
6189
|
-
throw new MTLSError(MTLSErrorType
|
|
3874
|
+
throw new MTLSError(MTLSErrorType.CONFIGURATION_ERROR, 'P12 certificate requires password for parsing');
|
|
6190
3875
|
}
|
|
6191
3876
|
result = await this.expoMTLS.parseCertificateP12(certificateData.certificate, certificateData.password);
|
|
6192
3877
|
}
|
|
@@ -6194,31 +3879,31 @@ class ReactNativeMTLSAdapter {
|
|
|
6194
3879
|
result = await this.expoMTLS.parseCertificatePEM(certificateData.certificate);
|
|
6195
3880
|
}
|
|
6196
3881
|
else {
|
|
6197
|
-
throw new MTLSError(MTLSErrorType
|
|
3882
|
+
throw new MTLSError(MTLSErrorType.CERTIFICATE_INVALID, `Unsupported certificate format: ${certificateData.format}`);
|
|
6198
3883
|
}
|
|
6199
|
-
log$
|
|
3884
|
+
log$5.debug('Certificate parsed successfully:', {
|
|
6200
3885
|
certificateCount: result.certificates.length,
|
|
6201
3886
|
subjects: result.certificates.map((cert) => cert.subject.commonName),
|
|
6202
3887
|
});
|
|
6203
3888
|
return result;
|
|
6204
3889
|
}
|
|
6205
3890
|
catch (error) {
|
|
6206
|
-
log$
|
|
3891
|
+
log$5.error('Failed to parse certificate:', error);
|
|
6207
3892
|
if (error instanceof MTLSError) {
|
|
6208
3893
|
throw error;
|
|
6209
3894
|
}
|
|
6210
|
-
throw new MTLSError(MTLSErrorType
|
|
3895
|
+
throw new MTLSError(MTLSErrorType.CERTIFICATE_INVALID, 'Failed to parse certificate data', error);
|
|
6211
3896
|
}
|
|
6212
3897
|
}
|
|
6213
3898
|
async request(requestConfig) {
|
|
6214
3899
|
if (!this.expoMTLS) {
|
|
6215
|
-
throw new MTLSError(MTLSErrorType
|
|
3900
|
+
throw new MTLSError(MTLSErrorType.NOT_SUPPORTED, 'Expo mTLS module not available');
|
|
6216
3901
|
}
|
|
6217
3902
|
const hasCert = await this.hasCertificate();
|
|
6218
3903
|
if (!hasCert) {
|
|
6219
|
-
throw new MTLSError(MTLSErrorType
|
|
3904
|
+
throw new MTLSError(MTLSErrorType.CERTIFICATE_NOT_FOUND, 'No certificate configured');
|
|
6220
3905
|
}
|
|
6221
|
-
log$
|
|
3906
|
+
log$5.debug('Making mTLS request:', {
|
|
6222
3907
|
method: requestConfig.method || 'GET',
|
|
6223
3908
|
url: requestConfig.url,
|
|
6224
3909
|
headers: requestConfig.headers,
|
|
@@ -6226,7 +3911,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6226
3911
|
responseType: requestConfig.responseType,
|
|
6227
3912
|
});
|
|
6228
3913
|
if (requestConfig.data) {
|
|
6229
|
-
log$
|
|
3914
|
+
log$5.debug('mTLS request body:', requestConfig.data);
|
|
6230
3915
|
}
|
|
6231
3916
|
try {
|
|
6232
3917
|
const response = await this.expoMTLS.request(requestConfig.url, {
|
|
@@ -6235,9 +3920,9 @@ class ReactNativeMTLSAdapter {
|
|
|
6235
3920
|
body: requestConfig.data ? JSON.stringify(requestConfig.data) : undefined,
|
|
6236
3921
|
responseType: requestConfig.responseType,
|
|
6237
3922
|
});
|
|
6238
|
-
log$
|
|
3923
|
+
log$5.debug('mTLS request successful:', response);
|
|
6239
3924
|
if (!response.success) {
|
|
6240
|
-
throw new MTLSError(MTLSErrorType
|
|
3925
|
+
throw new MTLSError(MTLSErrorType.CONNECTION_FAILED, `mTLS request failed: ${response.statusMessage} (${response.statusCode})`, undefined, response.statusCode);
|
|
6241
3926
|
}
|
|
6242
3927
|
let data = response.body;
|
|
6243
3928
|
// only parse if responseType is 'json' or if Content-Type header indicates JSON
|
|
@@ -6247,7 +3932,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6247
3932
|
data = JSON.parse(response.body);
|
|
6248
3933
|
}
|
|
6249
3934
|
catch (parseError) {
|
|
6250
|
-
log$
|
|
3935
|
+
log$5.warn('Failed to parse JSON response:', parseError);
|
|
6251
3936
|
// If parsing fails, keep raw body
|
|
6252
3937
|
}
|
|
6253
3938
|
}
|
|
@@ -6264,8 +3949,8 @@ class ReactNativeMTLSAdapter {
|
|
|
6264
3949
|
};
|
|
6265
3950
|
}
|
|
6266
3951
|
catch (error) {
|
|
6267
|
-
log$
|
|
6268
|
-
throw new MTLSError(MTLSErrorType
|
|
3952
|
+
log$5.error('mTLS request failed:', error);
|
|
3953
|
+
throw new MTLSError(MTLSErrorType.CONNECTION_FAILED, 'mTLS request failed', error);
|
|
6269
3954
|
}
|
|
6270
3955
|
}
|
|
6271
3956
|
/**
|
|
@@ -6277,18 +3962,18 @@ class ReactNativeMTLSAdapter {
|
|
|
6277
3962
|
*/
|
|
6278
3963
|
async testConnection() {
|
|
6279
3964
|
if (!this.expoMTLS || !this.config) {
|
|
6280
|
-
log$
|
|
3965
|
+
log$5.debug('Diagnostic test: No mTLS module or config available');
|
|
6281
3966
|
return false;
|
|
6282
3967
|
}
|
|
6283
3968
|
try {
|
|
6284
3969
|
const hasCert = await this.hasCertificate();
|
|
6285
3970
|
if (!hasCert) {
|
|
6286
|
-
log$
|
|
3971
|
+
log$5.debug('Diagnostic test: No certificate configured');
|
|
6287
3972
|
return false;
|
|
6288
3973
|
}
|
|
6289
|
-
log$
|
|
3974
|
+
log$5.debug('Running diagnostic test (may fail even if mTLS works):', this.config.baseUrl);
|
|
6290
3975
|
const result = await this.expoMTLS.testConnection(this.config.baseUrl);
|
|
6291
|
-
log$
|
|
3976
|
+
log$5.debug('Diagnostic test result (NOT validation):', {
|
|
6292
3977
|
success: result.success,
|
|
6293
3978
|
statusCode: result.statusCode,
|
|
6294
3979
|
statusMessage: result.statusMessage,
|
|
@@ -6299,13 +3984,13 @@ class ReactNativeMTLSAdapter {
|
|
|
6299
3984
|
return result.success;
|
|
6300
3985
|
}
|
|
6301
3986
|
catch (error) {
|
|
6302
|
-
log$
|
|
3987
|
+
log$5.warn('Diagnostic test failed (this is expected):', error);
|
|
6303
3988
|
return false;
|
|
6304
3989
|
}
|
|
6305
3990
|
}
|
|
6306
3991
|
async removeCertificate() {
|
|
6307
3992
|
if (!this.expoMTLS) {
|
|
6308
|
-
log$
|
|
3993
|
+
log$5.debug('Remove certificate: Module not available');
|
|
6309
3994
|
return;
|
|
6310
3995
|
}
|
|
6311
3996
|
try {
|
|
@@ -6314,11 +3999,11 @@ class ReactNativeMTLSAdapter {
|
|
|
6314
3999
|
this.isConfigured = false;
|
|
6315
4000
|
// Cleanup event listeners
|
|
6316
4001
|
this.cleanupEventListeners();
|
|
6317
|
-
log$
|
|
4002
|
+
log$5.debug('Certificate removed successfully');
|
|
6318
4003
|
}
|
|
6319
4004
|
catch (error) {
|
|
6320
|
-
log$
|
|
6321
|
-
throw new MTLSError(MTLSErrorType
|
|
4005
|
+
log$5.error('Failed to remove certificate:', error);
|
|
4006
|
+
throw new MTLSError(MTLSErrorType.CONFIGURATION_ERROR, 'Failed to remove certificate', error);
|
|
6322
4007
|
}
|
|
6323
4008
|
}
|
|
6324
4009
|
/**
|
|
@@ -6326,7 +4011,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6326
4011
|
*/
|
|
6327
4012
|
cleanupEventListeners() {
|
|
6328
4013
|
if (this.eventListeners.length > 0) {
|
|
6329
|
-
log$
|
|
4014
|
+
log$5.debug(`Cleaning up ${this.eventListeners.length} event listeners`);
|
|
6330
4015
|
}
|
|
6331
4016
|
// Remove individual listeners if they have remove methods
|
|
6332
4017
|
this.eventListeners.forEach((listener) => {
|
|
@@ -6363,7 +4048,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6363
4048
|
}
|
|
6364
4049
|
}
|
|
6365
4050
|
|
|
6366
|
-
const log$
|
|
4051
|
+
const log$4 = createPrefixedLogger('WEB-MTLS');
|
|
6367
4052
|
/**
|
|
6368
4053
|
* Web mTLS Adapter - Graceful fallback for web browsers
|
|
6369
4054
|
*
|
|
@@ -6377,13 +4062,13 @@ const log$7 = createPrefixedLogger('WEB-MTLS');
|
|
|
6377
4062
|
*/
|
|
6378
4063
|
class WebMTLSAdapter {
|
|
6379
4064
|
constructor() {
|
|
6380
|
-
log$
|
|
6381
|
-
log$
|
|
4065
|
+
log$4.warn('Web browsers do not support programmatic mTLS configuration');
|
|
4066
|
+
log$4.info('Use JWT authentication or configure client certificates in browser settings');
|
|
6382
4067
|
}
|
|
6383
4068
|
async isMTLSSupported() {
|
|
6384
4069
|
// mTLS is not supported programmatically in web browsers
|
|
6385
4070
|
const supported = false;
|
|
6386
|
-
log$
|
|
4071
|
+
log$4.debug('mTLS support check:', {
|
|
6387
4072
|
supported,
|
|
6388
4073
|
platform: this.getPlatformInfo().platform,
|
|
6389
4074
|
reason: 'Browser security model prevents programmatic certificate configuration',
|
|
@@ -6392,14 +4077,14 @@ class WebMTLSAdapter {
|
|
|
6392
4077
|
return supported;
|
|
6393
4078
|
}
|
|
6394
4079
|
async initialize(config) {
|
|
6395
|
-
log$
|
|
4080
|
+
log$4.warn('Initialized but mTLS not available in web browsers:', {
|
|
6396
4081
|
baseUrl: config.baseUrl,
|
|
6397
4082
|
port: config.port,
|
|
6398
4083
|
recommendation: 'Use standard HTTPS with JWT authentication',
|
|
6399
4084
|
});
|
|
6400
4085
|
}
|
|
6401
4086
|
async configureCertificate(certificateData) {
|
|
6402
|
-
log$
|
|
4087
|
+
log$4.error('Certificate configuration attempted:', {
|
|
6403
4088
|
format: certificateData.format,
|
|
6404
4089
|
reason: 'Not supported in web browsers',
|
|
6405
4090
|
alternatives: [
|
|
@@ -6408,21 +4093,21 @@ class WebMTLSAdapter {
|
|
|
6408
4093
|
'Configure server-side proxy for certificate handling',
|
|
6409
4094
|
],
|
|
6410
4095
|
});
|
|
6411
|
-
throw new MTLSError(MTLSErrorType
|
|
4096
|
+
throw new MTLSError(MTLSErrorType.NOT_SUPPORTED, 'mTLS client certificate configuration is not supported in web browsers. ' +
|
|
6412
4097
|
'Web browsers manage client certificates through the browser certificate store. ' +
|
|
6413
4098
|
'Please use JWT authentication or import certificates manually into your browser.');
|
|
6414
4099
|
}
|
|
6415
4100
|
async hasCertificate() {
|
|
6416
4101
|
// We cannot detect if the browser has certificates configured
|
|
6417
|
-
log$
|
|
4102
|
+
log$4.debug('Certificate availability check: Cannot detect browser certificates programmatically');
|
|
6418
4103
|
return false;
|
|
6419
4104
|
}
|
|
6420
4105
|
async getCertificateInfo() {
|
|
6421
|
-
log$
|
|
4106
|
+
log$4.debug('Certificate info requested: Not accessible in web browsers');
|
|
6422
4107
|
return null;
|
|
6423
4108
|
}
|
|
6424
4109
|
async request(requestConfig) {
|
|
6425
|
-
log$
|
|
4110
|
+
log$4.error('mTLS request attempted:', {
|
|
6426
4111
|
method: requestConfig.method,
|
|
6427
4112
|
url: requestConfig.url,
|
|
6428
4113
|
reason: 'Not supported in web browsers',
|
|
@@ -6432,16 +4117,16 @@ class WebMTLSAdapter {
|
|
|
6432
4117
|
'Rely on browser-managed certificates (if configured by user)',
|
|
6433
4118
|
],
|
|
6434
4119
|
});
|
|
6435
|
-
throw new MTLSError(MTLSErrorType
|
|
4120
|
+
throw new MTLSError(MTLSErrorType.NOT_SUPPORTED, 'mTLS requests are not supported in web browsers via JavaScript. ' +
|
|
6436
4121
|
'Use standard HTTP client with JWT authentication, or ensure client certificates ' +
|
|
6437
4122
|
'are properly configured in the browser certificate store.');
|
|
6438
4123
|
}
|
|
6439
4124
|
async testConnection() {
|
|
6440
|
-
log$
|
|
4125
|
+
log$4.debug('Connection test: mTLS not available in web browsers');
|
|
6441
4126
|
return false;
|
|
6442
4127
|
}
|
|
6443
4128
|
async removeCertificate() {
|
|
6444
|
-
log$
|
|
4129
|
+
log$4.debug('Remove certificate: No certificates to remove (not supported in web browsers)');
|
|
6445
4130
|
// No-op - cannot remove certificates programmatically in browsers
|
|
6446
4131
|
}
|
|
6447
4132
|
/**
|
|
@@ -6449,7 +4134,7 @@ class WebMTLSAdapter {
|
|
|
6449
4134
|
* Always returns null for web browsers as mTLS is not supported
|
|
6450
4135
|
*/
|
|
6451
4136
|
getBaseUrl() {
|
|
6452
|
-
log$
|
|
4137
|
+
log$4.debug('Base URL requested: Not supported in web browsers');
|
|
6453
4138
|
return null;
|
|
6454
4139
|
}
|
|
6455
4140
|
getPlatformInfo() {
|
|
@@ -6482,7 +4167,7 @@ class WebMTLSAdapter {
|
|
|
6482
4167
|
}
|
|
6483
4168
|
}
|
|
6484
4169
|
|
|
6485
|
-
const log$
|
|
4170
|
+
const log$3 = createPrefixedLogger('MTLS-LOADER');
|
|
6486
4171
|
function loadMTLSAdapter(platform, config) {
|
|
6487
4172
|
try {
|
|
6488
4173
|
let adapter;
|
|
@@ -6516,7 +4201,7 @@ function loadMTLSAdapter(platform, config) {
|
|
|
6516
4201
|
return adapter;
|
|
6517
4202
|
}
|
|
6518
4203
|
catch (error) {
|
|
6519
|
-
log$
|
|
4204
|
+
log$3.warn(`mTLS adapter not available for platform ${platform}:`, error);
|
|
6520
4205
|
return null;
|
|
6521
4206
|
}
|
|
6522
4207
|
}
|
|
@@ -6526,7 +4211,7 @@ async function initializeAdapterAsync(adapter, config) {
|
|
|
6526
4211
|
if (isSupported) {
|
|
6527
4212
|
await adapter.initialize(config);
|
|
6528
4213
|
const platformInfo = adapter.getPlatformInfo();
|
|
6529
|
-
log$
|
|
4214
|
+
log$3.debug('mTLS adapter initialized:', {
|
|
6530
4215
|
platform: platformInfo.platform,
|
|
6531
4216
|
mtlsSupported: platformInfo.mtlsSupported,
|
|
6532
4217
|
certificateStorage: platformInfo.certificateStorage,
|
|
@@ -6535,31 +4220,28 @@ async function initializeAdapterAsync(adapter, config) {
|
|
|
6535
4220
|
}
|
|
6536
4221
|
}
|
|
6537
4222
|
catch (error) {
|
|
6538
|
-
log$
|
|
4223
|
+
log$3.warn('Failed to initialize mTLS adapter:', error);
|
|
6539
4224
|
}
|
|
6540
4225
|
}
|
|
6541
4226
|
|
|
6542
|
-
const log$
|
|
4227
|
+
const log$2 = createPrefixedLogger('ADAPTER-LOADER');
|
|
6543
4228
|
function loadPlatformAdapters(options = {}) {
|
|
6544
4229
|
const { mtlsConfig } = options;
|
|
6545
4230
|
const { platform } = detectPlatform();
|
|
6546
|
-
log$
|
|
4231
|
+
log$2.debug('Loading adapters for platform:', platform);
|
|
6547
4232
|
const storageAdapters = loadStorageAdapters(platform);
|
|
6548
4233
|
const networkMonitor = loadNetworkMonitor(platform);
|
|
6549
|
-
const cache = loadCacheAdapter(platform);
|
|
6550
4234
|
const mtls = loadMTLSAdapter(platform, mtlsConfig);
|
|
6551
|
-
log$
|
|
4235
|
+
log$2.debug('Adapters loaded:', {
|
|
6552
4236
|
platform,
|
|
6553
4237
|
hasStorage: !!storageAdapters.storage,
|
|
6554
4238
|
hasSecureStorage: !!storageAdapters.secureStorage,
|
|
6555
4239
|
hasNetworkMonitor: !!networkMonitor,
|
|
6556
|
-
hasCache: !!cache,
|
|
6557
4240
|
hasMTLS: !!mtls,
|
|
6558
4241
|
});
|
|
6559
4242
|
return {
|
|
6560
4243
|
...storageAdapters,
|
|
6561
4244
|
networkMonitor,
|
|
6562
|
-
cache,
|
|
6563
4245
|
mtls: mtls || undefined,
|
|
6564
4246
|
};
|
|
6565
4247
|
}
|
|
@@ -6576,12 +4258,9 @@ function createACubeMTLSConfig(baseUrl, timeout, autoInitialize = true, forcePor
|
|
|
6576
4258
|
|
|
6577
4259
|
const DI_TOKENS = {
|
|
6578
4260
|
HTTP_PORT: Symbol('HTTP_PORT'),
|
|
6579
|
-
BASE_HTTP_PORT: Symbol('BASE_HTTP_PORT'),
|
|
6580
4261
|
STORAGE_PORT: Symbol('STORAGE_PORT'),
|
|
6581
4262
|
SECURE_STORAGE_PORT: Symbol('SECURE_STORAGE_PORT'),
|
|
6582
4263
|
NETWORK_PORT: Symbol('NETWORK_PORT'),
|
|
6583
|
-
CACHE_PORT: Symbol('CACHE_PORT'),
|
|
6584
|
-
CACHE_KEY_GENERATOR: Symbol('CACHE_KEY_GENERATOR'),
|
|
6585
4264
|
MTLS_PORT: Symbol('MTLS_PORT'),
|
|
6586
4265
|
TOKEN_STORAGE_PORT: Symbol('TOKEN_STORAGE_PORT'),
|
|
6587
4266
|
RECEIPT_REPOSITORY: Symbol('RECEIPT_REPOSITORY'),
|
|
@@ -6599,7 +4278,6 @@ const DI_TOKENS = {
|
|
|
6599
4278
|
AUTH_SERVICE: Symbol('AUTH_SERVICE'),
|
|
6600
4279
|
AUTHENTICATION_SERVICE: Symbol('AUTHENTICATION_SERVICE'),
|
|
6601
4280
|
CERTIFICATE_SERVICE: Symbol('CERTIFICATE_SERVICE'),
|
|
6602
|
-
OFFLINE_SERVICE: Symbol('OFFLINE_SERVICE'),
|
|
6603
4281
|
NOTIFICATION_SERVICE: Symbol('NOTIFICATION_SERVICE'),
|
|
6604
4282
|
TELEMETRY_SERVICE: Symbol('TELEMETRY_SERVICE'),
|
|
6605
4283
|
};
|
|
@@ -7494,545 +5172,83 @@ class TelemetryMapper {
|
|
|
7494
5172
|
: null,
|
|
7495
5173
|
lastMessageFromAde: output.last_message_from_ade
|
|
7496
5174
|
? this.messageFromApi(output.last_message_from_ade)
|
|
7497
|
-
: null,
|
|
7498
|
-
lottery: this.lotteryFromApi(output.lottery),
|
|
7499
|
-
};
|
|
7500
|
-
}
|
|
7501
|
-
static merchantFromApi(output) {
|
|
7502
|
-
return {
|
|
7503
|
-
vatNumber: output.vat_number,
|
|
7504
|
-
fiscalCode: output.fiscal_code,
|
|
7505
|
-
businessName: output.business_name,
|
|
7506
|
-
};
|
|
7507
|
-
}
|
|
7508
|
-
static supplierFromApi(output) {
|
|
7509
|
-
return {
|
|
7510
|
-
vatNumber: output.vat_number,
|
|
7511
|
-
fiscalCode: output.fiscal_code,
|
|
7512
|
-
businessName: output.business_name,
|
|
7513
|
-
};
|
|
7514
|
-
}
|
|
7515
|
-
static softwareVersionFromApi(output) {
|
|
7516
|
-
return {
|
|
7517
|
-
version: output.version,
|
|
7518
|
-
swid: output.swid,
|
|
7519
|
-
installedAt: output.installed_at,
|
|
7520
|
-
status: output.status,
|
|
7521
|
-
};
|
|
7522
|
-
}
|
|
7523
|
-
static softwareFromApi(output) {
|
|
7524
|
-
return {
|
|
7525
|
-
code: output.code,
|
|
7526
|
-
name: output.name,
|
|
7527
|
-
approvalReference: output.approval_reference,
|
|
7528
|
-
versionInfo: output.version_info ? this.softwareVersionFromApi(output.version_info) : null,
|
|
7529
|
-
};
|
|
7530
|
-
}
|
|
7531
|
-
static pendingReceiptsFromApi(output) {
|
|
7532
|
-
return {
|
|
7533
|
-
count: output.count,
|
|
7534
|
-
totalAmount: output.total_amount,
|
|
7535
|
-
};
|
|
7536
|
-
}
|
|
7537
|
-
static transmissionFromApi(output) {
|
|
7538
|
-
return {
|
|
7539
|
-
attemptedAt: output.attempted_at,
|
|
7540
|
-
outcome: output.outcome,
|
|
7541
|
-
};
|
|
7542
|
-
}
|
|
7543
|
-
static messageFromApi(output) {
|
|
7544
|
-
return {
|
|
7545
|
-
receivedAt: output.received_at,
|
|
7546
|
-
content: output.content,
|
|
7547
|
-
};
|
|
7548
|
-
}
|
|
7549
|
-
static secretRequestFromApi(output) {
|
|
7550
|
-
return {
|
|
7551
|
-
requestedAt: output.requested_at,
|
|
7552
|
-
outcome: output.outcome,
|
|
7553
|
-
};
|
|
7554
|
-
}
|
|
7555
|
-
static lotteryFromApi(output) {
|
|
7556
|
-
return {
|
|
7557
|
-
lastTransmission: output.last_transmission
|
|
7558
|
-
? this.transmissionFromApi(output.last_transmission)
|
|
7559
|
-
: null,
|
|
7560
|
-
secretRequest: output.secret_request
|
|
7561
|
-
? this.secretRequestFromApi(output.secret_request)
|
|
7562
|
-
: null,
|
|
7563
|
-
};
|
|
7564
|
-
}
|
|
7565
|
-
}
|
|
7566
|
-
|
|
7567
|
-
class TelemetryRepositoryImpl {
|
|
7568
|
-
constructor(http) {
|
|
7569
|
-
this.http = http;
|
|
7570
|
-
}
|
|
7571
|
-
async getTelemetry(pemId) {
|
|
7572
|
-
const response = await this.http.get(`/mf1/pems/${pemId}/telemetry`);
|
|
7573
|
-
return TelemetryMapper.fromApiOutput(response.data);
|
|
7574
|
-
}
|
|
7575
|
-
}
|
|
7576
|
-
|
|
7577
|
-
const log$4 = createPrefixedLogger('CACHE-KEY');
|
|
7578
|
-
const URL_PATTERNS = [
|
|
7579
|
-
// Receipt (mf1) - specific patterns first
|
|
7580
|
-
{
|
|
7581
|
-
pattern: /^\/mf1\/receipts\/([^/]+)\/returnable-items$/,
|
|
7582
|
-
resource: 'receipt',
|
|
7583
|
-
action: 'returnable',
|
|
7584
|
-
},
|
|
7585
|
-
{
|
|
7586
|
-
pattern: /^\/mf1\/receipts\/([^/]+)\/details$/,
|
|
7587
|
-
resource: 'receipt',
|
|
7588
|
-
action: 'details',
|
|
7589
|
-
},
|
|
7590
|
-
{ pattern: /^\/mf1\/receipts\/([^/]+)$/, resource: 'receipt' },
|
|
7591
|
-
{
|
|
7592
|
-
pattern: /^\/mf1\/pems\/([^/]+)\/receipts$/,
|
|
7593
|
-
resource: 'receipt',
|
|
7594
|
-
parent: 'point-of-sale',
|
|
7595
|
-
isList: true,
|
|
7596
|
-
},
|
|
7597
|
-
{ pattern: /^\/mf1\/receipts$/, resource: 'receipt', isList: true },
|
|
7598
|
-
// Merchant (mf2)
|
|
7599
|
-
{ pattern: /^\/mf2\/merchants\/([^/]+)$/, resource: 'merchant' },
|
|
7600
|
-
{ pattern: /^\/mf2\/merchants$/, resource: 'merchant', isList: true },
|
|
7601
|
-
// Cashier (mf1)
|
|
7602
|
-
{ pattern: /^\/mf1\/cashiers\/me$/, resource: 'cashier', action: 'me' },
|
|
7603
|
-
{ pattern: /^\/mf1\/cashiers\/([^/]+)$/, resource: 'cashier' },
|
|
7604
|
-
{ pattern: /^\/mf1\/cashiers$/, resource: 'cashier', isList: true },
|
|
7605
|
-
// Cash Register (mf1)
|
|
7606
|
-
{ pattern: /^\/mf1\/cash-registers\/([^/]+)$/, resource: 'cash-register' },
|
|
7607
|
-
{ pattern: /^\/mf1\/cash-registers$/, resource: 'cash-register', isList: true },
|
|
7608
|
-
// Point of Sale (mf1)
|
|
7609
|
-
{ pattern: /^\/mf1\/pems\/([^/]+)$/, resource: 'point-of-sale' },
|
|
7610
|
-
{ pattern: /^\/mf1\/pems$/, resource: 'point-of-sale', isList: true },
|
|
7611
|
-
// Nested resources under merchant (mf2)
|
|
7612
|
-
{
|
|
7613
|
-
pattern: /^\/mf2\/merchants\/([^/]+)\/suppliers\/([^/]+)$/,
|
|
7614
|
-
resource: 'supplier',
|
|
7615
|
-
parent: 'merchant',
|
|
7616
|
-
},
|
|
7617
|
-
{
|
|
7618
|
-
pattern: /^\/mf2\/merchants\/([^/]+)\/suppliers$/,
|
|
7619
|
-
resource: 'supplier',
|
|
7620
|
-
parent: 'merchant',
|
|
7621
|
-
isList: true,
|
|
7622
|
-
},
|
|
7623
|
-
{
|
|
7624
|
-
pattern: /^\/mf2\/merchants\/([^/]+)\/daily-reports/,
|
|
7625
|
-
resource: 'daily-report',
|
|
7626
|
-
parent: 'merchant',
|
|
7627
|
-
isList: true,
|
|
7628
|
-
},
|
|
7629
|
-
{
|
|
7630
|
-
pattern: /^\/mf2\/merchants\/([^/]+)\/journals/,
|
|
7631
|
-
resource: 'journal',
|
|
7632
|
-
parent: 'merchant',
|
|
7633
|
-
isList: true,
|
|
7634
|
-
},
|
|
7635
|
-
// PEM (mf2)
|
|
7636
|
-
{
|
|
7637
|
-
pattern: /^\/mf2\/pems\/([^/]+)\/certificates$/,
|
|
7638
|
-
resource: 'pem',
|
|
7639
|
-
action: 'certificates',
|
|
7640
|
-
},
|
|
7641
|
-
{ pattern: /^\/mf2\/pems\/([^/]+)$/, resource: 'pem' },
|
|
7642
|
-
// Others
|
|
7643
|
-
{ pattern: /^\/mf1\/notifications/, resource: 'notification', isList: true },
|
|
7644
|
-
{
|
|
7645
|
-
pattern: /^\/mf1\/pems\/([^/]+)\/telemetry$/,
|
|
7646
|
-
resource: 'telemetry',
|
|
7647
|
-
},
|
|
7648
|
-
];
|
|
7649
|
-
const DEFAULT_TTL_CONFIG = {
|
|
7650
|
-
// Data that rarely changes - 30 min TTL for items only
|
|
7651
|
-
merchant: { ttlMs: 30 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7652
|
-
'point-of-sale': { ttlMs: 30 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7653
|
-
'cash-register': { ttlMs: 30 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7654
|
-
pem: { ttlMs: 30 * 60 * 1000, cacheList: false, cacheItem: false },
|
|
7655
|
-
// Data that changes moderately - 10 min TTL for items only
|
|
7656
|
-
cashier: { ttlMs: 10 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7657
|
-
supplier: { ttlMs: 10 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7658
|
-
// Data that can change - 5 min TTL for items only
|
|
7659
|
-
receipt: { ttlMs: 5 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7660
|
-
'daily-report': { ttlMs: 5 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7661
|
-
journal: { ttlMs: 5 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7662
|
-
// Real-time data - 1 min TTL
|
|
7663
|
-
notification: { ttlMs: 1 * 60 * 1000, cacheList: false, cacheItem: false },
|
|
7664
|
-
telemetry: { ttlMs: 1 * 60 * 1000, cacheList: false, cacheItem: false },
|
|
7665
|
-
};
|
|
7666
|
-
const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
|
|
7667
|
-
class CacheKeyGenerator {
|
|
7668
|
-
constructor(customConfig) {
|
|
7669
|
-
this.config = { ...DEFAULT_TTL_CONFIG, ...customConfig };
|
|
7670
|
-
log$4.info('CacheKeyGenerator initialized with config:', {
|
|
7671
|
-
resources: Object.keys(this.config),
|
|
7672
|
-
});
|
|
7673
|
-
}
|
|
7674
|
-
generate(url, params) {
|
|
7675
|
-
const parsed = this.parseUrl(url);
|
|
7676
|
-
if (!parsed) {
|
|
7677
|
-
// Fallback: use URL as key
|
|
7678
|
-
const paramStr = params ? this.serializeParams(params) : '';
|
|
7679
|
-
const key = paramStr ? `${url}?${paramStr}` : url;
|
|
7680
|
-
log$4.debug('URL not matched, using fallback key:', { url, key });
|
|
7681
|
-
return key;
|
|
7682
|
-
}
|
|
7683
|
-
const { resource, ids, action, isList, parent } = parsed;
|
|
7684
|
-
if (isList) {
|
|
7685
|
-
const paramStr = params ? this.serializeParams(params) : '';
|
|
7686
|
-
const parentPart = parent && ids.length > 0 ? `${parent}=${ids[0]}&` : '';
|
|
7687
|
-
const key = `${resource}:list:${parentPart}${paramStr}`;
|
|
7688
|
-
log$4.debug('Generated list cache key:', { url, key, resource });
|
|
7689
|
-
return key;
|
|
7690
|
-
}
|
|
7691
|
-
// Single item
|
|
7692
|
-
if (ids.length === 0 && action) {
|
|
7693
|
-
// Special case for endpoints like /cashiers/me
|
|
7694
|
-
const key = `${resource}:${action}`;
|
|
7695
|
-
log$4.debug('Generated special action cache key:', { url, key, resource, action });
|
|
7696
|
-
return key;
|
|
7697
|
-
}
|
|
7698
|
-
let key = `${resource}:${ids.join(':')}`;
|
|
7699
|
-
if (action) {
|
|
7700
|
-
key += `:${action}`;
|
|
7701
|
-
}
|
|
7702
|
-
log$4.debug('Generated item cache key:', { url, key, resource, ids, action });
|
|
7703
|
-
return key;
|
|
7704
|
-
}
|
|
7705
|
-
parseResource(url) {
|
|
7706
|
-
const parsed = this.parseUrl(url);
|
|
7707
|
-
return parsed?.resource;
|
|
7708
|
-
}
|
|
7709
|
-
getTTL(url) {
|
|
7710
|
-
const resource = this.parseResource(url);
|
|
7711
|
-
if (!resource) {
|
|
7712
|
-
log$4.debug('No resource found for URL, using default TTL:', { url, ttl: DEFAULT_TTL });
|
|
7713
|
-
return DEFAULT_TTL;
|
|
7714
|
-
}
|
|
7715
|
-
const ttl = this.config[resource].ttlMs;
|
|
7716
|
-
log$4.debug('TTL for resource:', { url, resource, ttlMs: ttl, ttlMin: ttl / 60000 });
|
|
7717
|
-
return ttl;
|
|
7718
|
-
}
|
|
7719
|
-
shouldCache(url) {
|
|
7720
|
-
const parsed = this.parseUrl(url);
|
|
7721
|
-
if (!parsed) {
|
|
7722
|
-
log$4.debug('URL not recognized, should not cache:', { url });
|
|
7723
|
-
return false;
|
|
7724
|
-
}
|
|
7725
|
-
const { resource, isList } = parsed;
|
|
7726
|
-
const config = this.config[resource];
|
|
7727
|
-
if (isList) {
|
|
7728
|
-
log$4.debug('List endpoint cache decision:', {
|
|
7729
|
-
url,
|
|
7730
|
-
resource,
|
|
7731
|
-
isList: true,
|
|
7732
|
-
shouldCache: config.cacheList,
|
|
7733
|
-
});
|
|
7734
|
-
return config.cacheList;
|
|
7735
|
-
}
|
|
7736
|
-
log$4.debug('Item endpoint cache decision:', {
|
|
7737
|
-
url,
|
|
7738
|
-
resource,
|
|
7739
|
-
isList: false,
|
|
7740
|
-
shouldCache: config.cacheItem,
|
|
7741
|
-
});
|
|
7742
|
-
return config.cacheItem;
|
|
7743
|
-
}
|
|
7744
|
-
getInvalidationPatterns(url, method) {
|
|
7745
|
-
const parsed = this.parseUrl(url);
|
|
7746
|
-
if (!parsed) {
|
|
7747
|
-
log$4.debug('No patterns to invalidate for URL:', { url, method });
|
|
7748
|
-
return [];
|
|
7749
|
-
}
|
|
7750
|
-
const { resource, ids, parent } = parsed;
|
|
7751
|
-
const patterns = [];
|
|
7752
|
-
// Always invalidate list on mutations
|
|
7753
|
-
if (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
|
|
7754
|
-
if (parent && ids.length > 0) {
|
|
7755
|
-
patterns.push(`${resource}:list:${parent}=${ids[0]}*`);
|
|
7756
|
-
}
|
|
7757
|
-
patterns.push(`${resource}:list:*`);
|
|
7758
|
-
}
|
|
7759
|
-
// Invalidate specific item on PUT/PATCH/DELETE
|
|
7760
|
-
if (method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
|
|
7761
|
-
if (ids.length > 0) {
|
|
7762
|
-
patterns.push(`${resource}:${ids.join(':')}*`);
|
|
7763
|
-
}
|
|
7764
|
-
}
|
|
7765
|
-
// Special cases
|
|
7766
|
-
if (resource === 'cashier' && (method === 'PUT' || method === 'DELETE')) {
|
|
7767
|
-
patterns.push('cashier:me');
|
|
7768
|
-
}
|
|
7769
|
-
log$4.debug('Invalidation patterns:', { url, method, patterns });
|
|
7770
|
-
return patterns;
|
|
7771
|
-
}
|
|
7772
|
-
parseUrl(url) {
|
|
7773
|
-
// Remove query string for pattern matching
|
|
7774
|
-
const urlPath = url.split('?')[0];
|
|
7775
|
-
for (const pattern of URL_PATTERNS) {
|
|
7776
|
-
const match = urlPath?.match(pattern.pattern);
|
|
7777
|
-
if (match) {
|
|
7778
|
-
// Extract IDs from capture groups
|
|
7779
|
-
const ids = match.slice(1).filter(Boolean);
|
|
7780
|
-
return {
|
|
7781
|
-
resource: pattern.resource,
|
|
7782
|
-
ids,
|
|
7783
|
-
action: pattern.action,
|
|
7784
|
-
isList: pattern.isList,
|
|
7785
|
-
parent: pattern.parent,
|
|
7786
|
-
};
|
|
7787
|
-
}
|
|
7788
|
-
}
|
|
7789
|
-
return null;
|
|
7790
|
-
}
|
|
7791
|
-
serializeParams(params) {
|
|
7792
|
-
const sortedKeys = Object.keys(params).sort();
|
|
7793
|
-
const parts = [];
|
|
7794
|
-
for (const key of sortedKeys) {
|
|
7795
|
-
const value = params[key];
|
|
7796
|
-
if (value !== undefined && value !== null) {
|
|
7797
|
-
parts.push(`${key}=${String(value)}`);
|
|
7798
|
-
}
|
|
7799
|
-
}
|
|
7800
|
-
return parts.join('&');
|
|
7801
|
-
}
|
|
7802
|
-
}
|
|
7803
|
-
|
|
7804
|
-
const log$3 = createPrefixedLogger('CACHE');
|
|
7805
|
-
class CachingHttpDecorator {
|
|
7806
|
-
constructor(http, cache, keyGenerator, networkMonitor, config = {}) {
|
|
7807
|
-
this.http = http;
|
|
7808
|
-
this.cache = cache;
|
|
7809
|
-
this.keyGenerator = keyGenerator;
|
|
7810
|
-
this.networkMonitor = networkMonitor;
|
|
7811
|
-
this.config = config;
|
|
7812
|
-
this.currentOnlineState = true;
|
|
7813
|
-
this.authToken = null;
|
|
7814
|
-
log$3.info('CachingHttpDecorator initialized', {
|
|
7815
|
-
enabled: config.enabled !== false,
|
|
7816
|
-
hasNetworkMonitor: !!networkMonitor,
|
|
7817
|
-
});
|
|
7818
|
-
this.setupNetworkMonitoring();
|
|
5175
|
+
: null,
|
|
5176
|
+
lottery: this.lotteryFromApi(output.lottery),
|
|
5177
|
+
};
|
|
7819
5178
|
}
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
this.currentOnlineState = online;
|
|
7827
|
-
});
|
|
7828
|
-
}
|
|
5179
|
+
static merchantFromApi(output) {
|
|
5180
|
+
return {
|
|
5181
|
+
vatNumber: output.vat_number,
|
|
5182
|
+
fiscalCode: output.fiscal_code,
|
|
5183
|
+
businessName: output.business_name,
|
|
5184
|
+
};
|
|
7829
5185
|
}
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
}
|
|
7837
|
-
return true;
|
|
5186
|
+
static supplierFromApi(output) {
|
|
5187
|
+
return {
|
|
5188
|
+
vatNumber: output.vat_number,
|
|
5189
|
+
fiscalCode: output.fiscal_code,
|
|
5190
|
+
businessName: output.business_name,
|
|
5191
|
+
};
|
|
7838
5192
|
}
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
}
|
|
7846
|
-
// Check if this URL should be cached
|
|
7847
|
-
const shouldCache = this.keyGenerator.shouldCache(url);
|
|
7848
|
-
if (!shouldCache) {
|
|
7849
|
-
log$3.debug('GET (not cacheable - likely a list endpoint):', { url });
|
|
7850
|
-
return this.http.get(url, config);
|
|
7851
|
-
}
|
|
7852
|
-
const cacheKey = this.keyGenerator.generate(url, config?.params);
|
|
7853
|
-
const ttl = this.keyGenerator.getTTL(url);
|
|
7854
|
-
const resource = this.keyGenerator.parseResource(url);
|
|
7855
|
-
log$3.info('GET request starting:', {
|
|
7856
|
-
url,
|
|
7857
|
-
resource,
|
|
7858
|
-
cacheKey,
|
|
7859
|
-
ttlMs: ttl,
|
|
7860
|
-
ttlMin: Math.round(ttl / 60000),
|
|
7861
|
-
params: config?.params,
|
|
7862
|
-
});
|
|
7863
|
-
// Check cache
|
|
7864
|
-
let cached = null;
|
|
7865
|
-
try {
|
|
7866
|
-
cached = await this.cache.get(cacheKey);
|
|
7867
|
-
if (cached) {
|
|
7868
|
-
log$3.debug('Cache entry found:', {
|
|
7869
|
-
cacheKey,
|
|
7870
|
-
timestamp: new Date(cached.timestamp).toISOString(),
|
|
7871
|
-
ageMs: Date.now() - cached.timestamp,
|
|
7872
|
-
});
|
|
7873
|
-
}
|
|
7874
|
-
else {
|
|
7875
|
-
log$3.debug('Cache entry not found:', { cacheKey });
|
|
7876
|
-
}
|
|
7877
|
-
}
|
|
7878
|
-
catch (error) {
|
|
7879
|
-
log$3.warn('Cache lookup failed:', {
|
|
7880
|
-
cacheKey,
|
|
7881
|
-
error: error instanceof Error ? error.message : error,
|
|
7882
|
-
});
|
|
7883
|
-
}
|
|
7884
|
-
if (cached) {
|
|
7885
|
-
const age = Date.now() - cached.timestamp;
|
|
7886
|
-
const isExpired = age >= ttl;
|
|
7887
|
-
log$3.debug('Cache analysis:', {
|
|
7888
|
-
cacheKey,
|
|
7889
|
-
ageMs: age,
|
|
7890
|
-
ageSec: Math.round(age / 1000),
|
|
7891
|
-
ttlMs: ttl,
|
|
7892
|
-
isExpired,
|
|
7893
|
-
isOnline: this.isOnline(),
|
|
7894
|
-
});
|
|
7895
|
-
// If within TTL, return cached data
|
|
7896
|
-
if (!isExpired) {
|
|
7897
|
-
const duration = Date.now() - startTime;
|
|
7898
|
-
log$3.info('CACHE HIT:', {
|
|
7899
|
-
url,
|
|
7900
|
-
cacheKey,
|
|
7901
|
-
ageMs: age,
|
|
7902
|
-
durationMs: duration,
|
|
7903
|
-
});
|
|
7904
|
-
return {
|
|
7905
|
-
data: cached.data,
|
|
7906
|
-
status: 200,
|
|
7907
|
-
headers: { 'x-cache': 'HIT' },
|
|
7908
|
-
};
|
|
7909
|
-
}
|
|
7910
|
-
// If offline and cache is stale, return stale data
|
|
7911
|
-
if (!this.isOnline()) {
|
|
7912
|
-
const duration = Date.now() - startTime;
|
|
7913
|
-
log$3.info('CACHE STALE (offline):', {
|
|
7914
|
-
url,
|
|
7915
|
-
cacheKey,
|
|
7916
|
-
ageMs: age,
|
|
7917
|
-
durationMs: duration,
|
|
7918
|
-
});
|
|
7919
|
-
return {
|
|
7920
|
-
data: cached.data,
|
|
7921
|
-
status: 200,
|
|
7922
|
-
headers: { 'x-cache': 'STALE' },
|
|
7923
|
-
};
|
|
7924
|
-
}
|
|
7925
|
-
log$3.debug('Cache expired, fetching fresh data:', { cacheKey, ageMs: age, ttlMs: ttl });
|
|
7926
|
-
}
|
|
7927
|
-
// Fetch fresh data
|
|
7928
|
-
try {
|
|
7929
|
-
log$3.debug('Fetching from network:', { url });
|
|
7930
|
-
const response = await this.http.get(url, config);
|
|
7931
|
-
// Cache the response
|
|
7932
|
-
try {
|
|
7933
|
-
await this.cache.set(cacheKey, response.data);
|
|
7934
|
-
log$3.debug('Response cached successfully:', { cacheKey });
|
|
7935
|
-
}
|
|
7936
|
-
catch (error) {
|
|
7937
|
-
log$3.error('Failed to cache response:', {
|
|
7938
|
-
cacheKey,
|
|
7939
|
-
error: error instanceof Error ? error.message : error,
|
|
7940
|
-
});
|
|
7941
|
-
}
|
|
7942
|
-
const duration = Date.now() - startTime;
|
|
7943
|
-
log$3.info('CACHE MISS (fetched fresh):', {
|
|
7944
|
-
url,
|
|
7945
|
-
cacheKey,
|
|
7946
|
-
status: response.status,
|
|
7947
|
-
durationMs: duration,
|
|
7948
|
-
});
|
|
7949
|
-
return {
|
|
7950
|
-
...response,
|
|
7951
|
-
headers: { ...response.headers, 'x-cache': 'MISS' },
|
|
7952
|
-
};
|
|
7953
|
-
}
|
|
7954
|
-
catch (error) {
|
|
7955
|
-
// On error, return stale cache if available
|
|
7956
|
-
if (cached) {
|
|
7957
|
-
const duration = Date.now() - startTime;
|
|
7958
|
-
log$3.warn('CACHE STALE (network error):', {
|
|
7959
|
-
url,
|
|
7960
|
-
cacheKey,
|
|
7961
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
7962
|
-
durationMs: duration,
|
|
7963
|
-
});
|
|
7964
|
-
return {
|
|
7965
|
-
data: cached.data,
|
|
7966
|
-
status: 200,
|
|
7967
|
-
headers: { 'x-cache': 'STALE' },
|
|
7968
|
-
};
|
|
7969
|
-
}
|
|
7970
|
-
log$3.error('Network error with no cache fallback:', {
|
|
7971
|
-
url,
|
|
7972
|
-
error: error instanceof Error ? error.message : error,
|
|
7973
|
-
});
|
|
7974
|
-
throw error;
|
|
7975
|
-
}
|
|
5193
|
+
static softwareVersionFromApi(output) {
|
|
5194
|
+
return {
|
|
5195
|
+
version: output.version,
|
|
5196
|
+
swid: output.swid,
|
|
5197
|
+
installedAt: output.installed_at,
|
|
5198
|
+
status: output.status,
|
|
5199
|
+
};
|
|
7976
5200
|
}
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
5201
|
+
static softwareFromApi(output) {
|
|
5202
|
+
return {
|
|
5203
|
+
code: output.code,
|
|
5204
|
+
name: output.name,
|
|
5205
|
+
approvalReference: output.approval_reference,
|
|
5206
|
+
versionInfo: output.version_info ? this.softwareVersionFromApi(output.version_info) : null,
|
|
5207
|
+
};
|
|
7982
5208
|
}
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
5209
|
+
static pendingReceiptsFromApi(output) {
|
|
5210
|
+
return {
|
|
5211
|
+
count: output.count,
|
|
5212
|
+
totalAmount: output.total_amount,
|
|
5213
|
+
};
|
|
7988
5214
|
}
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
5215
|
+
static transmissionFromApi(output) {
|
|
5216
|
+
return {
|
|
5217
|
+
attemptedAt: output.attempted_at,
|
|
5218
|
+
outcome: output.outcome,
|
|
5219
|
+
};
|
|
7994
5220
|
}
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
5221
|
+
static messageFromApi(output) {
|
|
5222
|
+
return {
|
|
5223
|
+
receivedAt: output.received_at,
|
|
5224
|
+
content: output.content,
|
|
5225
|
+
};
|
|
8000
5226
|
}
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
});
|
|
8007
|
-
this.authToken = token;
|
|
8008
|
-
this.http.setAuthToken(token);
|
|
5227
|
+
static secretRequestFromApi(output) {
|
|
5228
|
+
return {
|
|
5229
|
+
requestedAt: output.requested_at,
|
|
5230
|
+
outcome: output.outcome,
|
|
5231
|
+
};
|
|
8009
5232
|
}
|
|
8010
|
-
|
|
8011
|
-
return
|
|
5233
|
+
static lotteryFromApi(output) {
|
|
5234
|
+
return {
|
|
5235
|
+
lastTransmission: output.last_transmission
|
|
5236
|
+
? this.transmissionFromApi(output.last_transmission)
|
|
5237
|
+
: null,
|
|
5238
|
+
secretRequest: output.secret_request
|
|
5239
|
+
? this.secretRequestFromApi(output.secret_request)
|
|
5240
|
+
: null,
|
|
5241
|
+
};
|
|
8012
5242
|
}
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
}
|
|
8019
|
-
log$3.info('Invalidating cache patterns:', { url, method, patterns });
|
|
8020
|
-
for (const pattern of patterns) {
|
|
8021
|
-
try {
|
|
8022
|
-
await this.cache.invalidate(pattern);
|
|
8023
|
-
log$3.debug('Cache pattern invalidated:', { pattern });
|
|
8024
|
-
}
|
|
8025
|
-
catch (error) {
|
|
8026
|
-
log$3.error('Failed to invalidate pattern:', {
|
|
8027
|
-
pattern,
|
|
8028
|
-
error: error instanceof Error ? error.message : error,
|
|
8029
|
-
});
|
|
8030
|
-
}
|
|
8031
|
-
}
|
|
5243
|
+
}
|
|
5244
|
+
|
|
5245
|
+
class TelemetryRepositoryImpl {
|
|
5246
|
+
constructor(http) {
|
|
5247
|
+
this.http = http;
|
|
8032
5248
|
}
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
5249
|
+
async getTelemetry(pemId) {
|
|
5250
|
+
const response = await this.http.get(`/mf1/pems/${pemId}/telemetry`);
|
|
5251
|
+
return TelemetryMapper.fromApiOutput(response.data);
|
|
8036
5252
|
}
|
|
8037
5253
|
}
|
|
8038
5254
|
|
|
@@ -8305,7 +5521,6 @@ class SDKFactory {
|
|
|
8305
5521
|
baseUrl: config.baseUrl,
|
|
8306
5522
|
timeout: config.timeout,
|
|
8307
5523
|
});
|
|
8308
|
-
container.register(DI_TOKENS.BASE_HTTP_PORT, httpAdapter);
|
|
8309
5524
|
container.register(DI_TOKENS.HTTP_PORT, httpAdapter);
|
|
8310
5525
|
container.registerFactory(DI_TOKENS.RECEIPT_REPOSITORY, () => {
|
|
8311
5526
|
const http = container.get(DI_TOKENS.HTTP_PORT);
|
|
@@ -8353,23 +5568,6 @@ class SDKFactory {
|
|
|
8353
5568
|
});
|
|
8354
5569
|
return container;
|
|
8355
5570
|
}
|
|
8356
|
-
static registerCacheServices(container, cache, network) {
|
|
8357
|
-
container.register(DI_TOKENS.CACHE_PORT, cache);
|
|
8358
|
-
if (network) {
|
|
8359
|
-
container.register(DI_TOKENS.NETWORK_PORT, network);
|
|
8360
|
-
}
|
|
8361
|
-
const keyGenerator = new CacheKeyGenerator();
|
|
8362
|
-
container.register(DI_TOKENS.CACHE_KEY_GENERATOR, keyGenerator);
|
|
8363
|
-
const baseHttp = container.get(DI_TOKENS.BASE_HTTP_PORT);
|
|
8364
|
-
const cachingHttp = new CachingHttpDecorator(baseHttp, cache, keyGenerator, network);
|
|
8365
|
-
container.register(DI_TOKENS.HTTP_PORT, cachingHttp);
|
|
8366
|
-
}
|
|
8367
|
-
static getCacheKeyGenerator(container) {
|
|
8368
|
-
if (container.has(DI_TOKENS.CACHE_KEY_GENERATOR)) {
|
|
8369
|
-
return container.get(DI_TOKENS.CACHE_KEY_GENERATOR);
|
|
8370
|
-
}
|
|
8371
|
-
return undefined;
|
|
8372
|
-
}
|
|
8373
5571
|
static registerAuthServices(container, secureStorage, config) {
|
|
8374
5572
|
const tokenStorage = new TokenStorageAdapter(secureStorage);
|
|
8375
5573
|
container.register(DI_TOKENS.TOKEN_STORAGE_PORT, tokenStorage);
|
|
@@ -8407,7 +5605,7 @@ class SDKFactory {
|
|
|
8407
5605
|
}
|
|
8408
5606
|
}
|
|
8409
5607
|
|
|
8410
|
-
const log$
|
|
5608
|
+
const log$1 = createPrefixedLogger('SDK');
|
|
8411
5609
|
class ACubeSDK {
|
|
8412
5610
|
constructor(config, customAdapters, events = {}) {
|
|
8413
5611
|
this.events = events;
|
|
@@ -8421,23 +5619,22 @@ class ACubeSDK {
|
|
|
8421
5619
|
}
|
|
8422
5620
|
async initialize() {
|
|
8423
5621
|
if (this.isInitialized) {
|
|
8424
|
-
log$
|
|
5622
|
+
log$1.debug('SDK already initialized, skipping');
|
|
8425
5623
|
return;
|
|
8426
5624
|
}
|
|
8427
|
-
log$
|
|
5625
|
+
log$1.info('Initializing SDK', {
|
|
8428
5626
|
apiUrl: this.config.getApiUrl(),
|
|
8429
5627
|
authUrl: this.config.getAuthUrl(),
|
|
8430
5628
|
debugEnabled: this.config.isDebugEnabled(),
|
|
8431
5629
|
});
|
|
8432
5630
|
try {
|
|
8433
5631
|
if (!this.adapters) {
|
|
8434
|
-
log$
|
|
5632
|
+
log$1.debug('Loading platform adapters');
|
|
8435
5633
|
const mtlsConfig = createACubeMTLSConfig(this.config.getApiUrl(), this.config.getTimeout(), true);
|
|
8436
5634
|
this.adapters = loadPlatformAdapters({
|
|
8437
5635
|
mtlsConfig,
|
|
8438
5636
|
});
|
|
8439
|
-
log$
|
|
8440
|
-
hasCache: !!this.adapters.cache,
|
|
5637
|
+
log$1.info('Platform adapters loaded', {
|
|
8441
5638
|
hasNetworkMonitor: !!this.adapters.networkMonitor,
|
|
8442
5639
|
hasMtls: !!this.adapters.mtls,
|
|
8443
5640
|
hasSecureStorage: !!this.adapters.secureStorage,
|
|
@@ -8449,30 +5646,15 @@ class ACubeSDK {
|
|
|
8449
5646
|
timeout: this.config.getTimeout(),
|
|
8450
5647
|
debugEnabled: this.config.isDebugEnabled(),
|
|
8451
5648
|
};
|
|
8452
|
-
log$
|
|
5649
|
+
log$1.debug('Creating DI container');
|
|
8453
5650
|
this.container = SDKFactory.createContainer(factoryConfig);
|
|
8454
|
-
log$
|
|
5651
|
+
log$1.debug('Registering auth services');
|
|
8455
5652
|
SDKFactory.registerAuthServices(this.container, this.adapters.secureStorage, factoryConfig);
|
|
8456
|
-
|
|
8457
|
-
log$2.info('Registering cache services', {
|
|
8458
|
-
hasNetworkMonitor: !!this.adapters.networkMonitor,
|
|
8459
|
-
});
|
|
8460
|
-
SDKFactory.registerCacheServices(this.container, this.adapters.cache, this.adapters.networkMonitor);
|
|
8461
|
-
}
|
|
8462
|
-
else {
|
|
8463
|
-
log$2.debug('No cache adapter available, caching disabled');
|
|
8464
|
-
}
|
|
8465
|
-
log$2.debug('Initializing certificate service');
|
|
5653
|
+
log$1.debug('Initializing certificate service');
|
|
8466
5654
|
this.certificateService = new CertificateService(this.adapters.secureStorage);
|
|
8467
5655
|
const tokenStorage = this.container.get(DI_TOKENS.TOKEN_STORAGE_PORT);
|
|
8468
5656
|
const httpPort = this.container.get(DI_TOKENS.HTTP_PORT);
|
|
8469
|
-
|
|
8470
|
-
log$2.debug('HTTP ports initialized', {
|
|
8471
|
-
httpPortType: httpPort.constructor.name,
|
|
8472
|
-
baseHttpPortType: baseHttpPort.constructor.name,
|
|
8473
|
-
areSameInstance: httpPort === baseHttpPort,
|
|
8474
|
-
});
|
|
8475
|
-
log$2.debug('Initializing authentication service');
|
|
5657
|
+
log$1.debug('Initializing authentication service');
|
|
8476
5658
|
this.authService = new AuthenticationService(httpPort, tokenStorage, {
|
|
8477
5659
|
authUrl: this.config.getAuthUrl(),
|
|
8478
5660
|
timeout: this.config.getTimeout(),
|
|
@@ -8482,51 +5664,33 @@ class ACubeSDK {
|
|
|
8482
5664
|
this.events.onAuthError?.(new ACubeSDKError('AUTH_ERROR', error.message, error));
|
|
8483
5665
|
},
|
|
8484
5666
|
});
|
|
8485
|
-
log$2.debug('Initializing offline manager');
|
|
8486
|
-
const queueEvents = {
|
|
8487
|
-
onOperationAdded: (operation) => {
|
|
8488
|
-
this.events.onOfflineOperationAdded?.(operation.id);
|
|
8489
|
-
},
|
|
8490
|
-
onOperationCompleted: (result) => {
|
|
8491
|
-
this.events.onOfflineOperationCompleted?.(result.operation.id, result.success);
|
|
8492
|
-
},
|
|
8493
|
-
onOperationFailed: (result) => {
|
|
8494
|
-
this.events.onOfflineOperationCompleted?.(result.operation.id, false);
|
|
8495
|
-
},
|
|
8496
|
-
};
|
|
8497
|
-
this.offlineManager = new OfflineManager(this.adapters.storage, httpPort, this.adapters.networkMonitor, {
|
|
8498
|
-
syncInterval: 30000,
|
|
8499
|
-
}, queueEvents);
|
|
8500
5667
|
this.networkSubscription = this.adapters.networkMonitor.online$.subscribe((online) => {
|
|
8501
5668
|
this.currentOnlineState = online;
|
|
8502
5669
|
this.events.onNetworkStatusChanged?.(online);
|
|
8503
|
-
if (online && this.offlineManager) {
|
|
8504
|
-
this.offlineManager.sync().catch(() => { });
|
|
8505
|
-
}
|
|
8506
5670
|
});
|
|
8507
5671
|
const isAuth = await this.authService.isAuthenticated();
|
|
8508
|
-
log$
|
|
5672
|
+
log$1.debug('Checking authentication status during init', { isAuthenticated: isAuth });
|
|
8509
5673
|
if (isAuth) {
|
|
8510
5674
|
const token = await this.authService.getAccessToken();
|
|
8511
|
-
log$
|
|
5675
|
+
log$1.debug('Token retrieved during init', {
|
|
8512
5676
|
hasToken: !!token,
|
|
8513
5677
|
tokenPrefix: token?.substring(0, 20),
|
|
8514
5678
|
});
|
|
8515
5679
|
if (token) {
|
|
8516
5680
|
httpPort.setAuthToken(token);
|
|
8517
|
-
log$
|
|
5681
|
+
log$1.info('Auth token set on HTTP port during initialization');
|
|
8518
5682
|
}
|
|
8519
5683
|
}
|
|
8520
5684
|
else {
|
|
8521
|
-
log$
|
|
5685
|
+
log$1.warn('User not authenticated during SDK init - token will be set after login');
|
|
8522
5686
|
}
|
|
8523
|
-
if (this.adapters?.mtls && 'setMTLSAdapter' in
|
|
8524
|
-
log$
|
|
8525
|
-
const httpWithMtls =
|
|
5687
|
+
if (this.adapters?.mtls && 'setMTLSAdapter' in httpPort) {
|
|
5688
|
+
log$1.debug('Connecting mTLS adapter to HTTP port');
|
|
5689
|
+
const httpWithMtls = httpPort;
|
|
8526
5690
|
httpWithMtls.setMTLSAdapter(this.adapters.mtls);
|
|
8527
5691
|
}
|
|
8528
|
-
if ('setAuthStrategy' in
|
|
8529
|
-
log$
|
|
5692
|
+
if ('setAuthStrategy' in httpPort) {
|
|
5693
|
+
log$1.debug('Configuring auth strategy');
|
|
8530
5694
|
const jwtHandler = new JwtAuthHandler(tokenStorage);
|
|
8531
5695
|
const certificatePort = this.certificateService
|
|
8532
5696
|
? {
|
|
@@ -8557,7 +5721,7 @@ class ACubeSDK {
|
|
|
8557
5721
|
},
|
|
8558
5722
|
};
|
|
8559
5723
|
const authStrategy = new AuthStrategy(jwtHandler, mtlsHandler, userProvider, this.adapters?.mtls || null);
|
|
8560
|
-
const httpWithStrategy =
|
|
5724
|
+
const httpWithStrategy = httpPort;
|
|
8561
5725
|
httpWithStrategy.setAuthStrategy(authStrategy);
|
|
8562
5726
|
}
|
|
8563
5727
|
if (this.adapters?.mtls && this.certificateService) {
|
|
@@ -8575,19 +5739,18 @@ class ACubeSDK {
|
|
|
8575
5739
|
}
|
|
8576
5740
|
}
|
|
8577
5741
|
catch (certError) {
|
|
8578
|
-
log$
|
|
5742
|
+
log$1.warn('Certificate auto-configuration failed, will retry on demand', {
|
|
8579
5743
|
error: certError instanceof Error ? certError.message : certError,
|
|
8580
5744
|
});
|
|
8581
5745
|
}
|
|
8582
5746
|
}
|
|
8583
5747
|
this.isInitialized = true;
|
|
8584
|
-
log$
|
|
8585
|
-
hasCache: !!this.adapters.cache,
|
|
5748
|
+
log$1.info('SDK initialized successfully', {
|
|
8586
5749
|
hasMtls: !!this.adapters.mtls,
|
|
8587
5750
|
});
|
|
8588
5751
|
}
|
|
8589
5752
|
catch (error) {
|
|
8590
|
-
log$
|
|
5753
|
+
log$1.error('SDK initialization failed', {
|
|
8591
5754
|
error: error instanceof Error ? error.message : error,
|
|
8592
5755
|
});
|
|
8593
5756
|
throw new ACubeSDKError('SDK_INITIALIZATION_ERROR', `Failed to initialize SDK: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
|
|
@@ -8643,19 +5806,19 @@ class ACubeSDK {
|
|
|
8643
5806
|
}
|
|
8644
5807
|
async login(credentials) {
|
|
8645
5808
|
this.ensureInitialized();
|
|
8646
|
-
log$
|
|
5809
|
+
log$1.info('Login attempt', { email: credentials.email });
|
|
8647
5810
|
const user = await this.authService.login(credentials);
|
|
8648
|
-
log$
|
|
5811
|
+
log$1.info('Login successful', { roles: user.roles });
|
|
8649
5812
|
const token = await this.authService.getAccessToken();
|
|
8650
5813
|
if (token) {
|
|
8651
5814
|
this.httpPort.setAuthToken(token);
|
|
8652
|
-
log$
|
|
5815
|
+
log$1.debug('Auth token set on HTTP port');
|
|
8653
5816
|
}
|
|
8654
5817
|
return user;
|
|
8655
5818
|
}
|
|
8656
5819
|
async logout() {
|
|
8657
5820
|
this.ensureInitialized();
|
|
8658
|
-
log$
|
|
5821
|
+
log$1.info('Logout');
|
|
8659
5822
|
await this.authService.logout();
|
|
8660
5823
|
this.httpPort.setAuthToken(null);
|
|
8661
5824
|
}
|
|
@@ -8673,10 +5836,6 @@ class ACubeSDK {
|
|
|
8673
5836
|
this.ensureInitialized();
|
|
8674
5837
|
return await this.authService.isAuthenticated();
|
|
8675
5838
|
}
|
|
8676
|
-
getOfflineManager() {
|
|
8677
|
-
this.ensureInitialized();
|
|
8678
|
-
return this.offlineManager;
|
|
8679
|
-
}
|
|
8680
5839
|
isOnline() {
|
|
8681
5840
|
this.ensureInitialized();
|
|
8682
5841
|
return this.currentOnlineState;
|
|
@@ -8804,7 +5963,6 @@ class ACubeSDK {
|
|
|
8804
5963
|
}
|
|
8805
5964
|
destroy() {
|
|
8806
5965
|
this.networkSubscription?.unsubscribe();
|
|
8807
|
-
this.offlineManager?.destroy();
|
|
8808
5966
|
this.container?.clear();
|
|
8809
5967
|
this.isInitialized = false;
|
|
8810
5968
|
}
|
|
@@ -9090,7 +6248,6 @@ class TelemetryService {
|
|
|
9090
6248
|
this.events = events;
|
|
9091
6249
|
this.stateSubject = new BehaviorSubject({
|
|
9092
6250
|
data: null,
|
|
9093
|
-
isCached: false,
|
|
9094
6251
|
isLoading: false,
|
|
9095
6252
|
lastFetchedAt: null,
|
|
9096
6253
|
});
|
|
@@ -9160,7 +6317,6 @@ class TelemetryService {
|
|
|
9160
6317
|
const data = await this.repository.getTelemetry(this.currentPemId);
|
|
9161
6318
|
const newState = {
|
|
9162
6319
|
data,
|
|
9163
|
-
isCached: false,
|
|
9164
6320
|
isLoading: false,
|
|
9165
6321
|
lastFetchedAt: Date.now(),
|
|
9166
6322
|
};
|
|
@@ -9185,7 +6341,6 @@ class TelemetryService {
|
|
|
9185
6341
|
clearTelemetry() {
|
|
9186
6342
|
this.stateSubject.next({
|
|
9187
6343
|
data: null,
|
|
9188
|
-
isCached: false,
|
|
9189
6344
|
isLoading: false,
|
|
9190
6345
|
lastFetchedAt: null,
|
|
9191
6346
|
});
|
|
@@ -9198,7 +6353,7 @@ class TelemetryService {
|
|
|
9198
6353
|
}
|
|
9199
6354
|
}
|
|
9200
6355
|
|
|
9201
|
-
const log
|
|
6356
|
+
const log = createPrefixedLogger('SDK-MANAGER');
|
|
9202
6357
|
/**
|
|
9203
6358
|
* SDKManager - Singleton wrapper for ACubeSDK with simplified API
|
|
9204
6359
|
*
|
|
@@ -9265,7 +6420,7 @@ class SDKManager {
|
|
|
9265
6420
|
if (canPoll && !this.isPollingActive) {
|
|
9266
6421
|
const hasCert = await this.checkCertificate();
|
|
9267
6422
|
if (!hasCert) {
|
|
9268
|
-
log
|
|
6423
|
+
log.warn('Certificate missing — polling blocked until certificate is installed');
|
|
9269
6424
|
return;
|
|
9270
6425
|
}
|
|
9271
6426
|
this.notificationService?.startPolling();
|
|
@@ -9367,7 +6522,7 @@ class SDKManager {
|
|
|
9367
6522
|
this.isPollingActive = true;
|
|
9368
6523
|
}
|
|
9369
6524
|
else {
|
|
9370
|
-
log
|
|
6525
|
+
log.warn('Certificate missing at init — polling blocked until certificate is installed');
|
|
9371
6526
|
}
|
|
9372
6527
|
}
|
|
9373
6528
|
// AppStateService remains active for all users (handles OFFLINE network state)
|
|
@@ -9410,7 +6565,7 @@ class SDKManager {
|
|
|
9410
6565
|
return this.certificateMissingSubject.asObservable();
|
|
9411
6566
|
}
|
|
9412
6567
|
/**
|
|
9413
|
-
* Observable stream of telemetry state (data, isLoading,
|
|
6568
|
+
* Observable stream of telemetry state (data, isLoading, error)
|
|
9414
6569
|
*/
|
|
9415
6570
|
get telemetryState$() {
|
|
9416
6571
|
this.ensureInitialized();
|
|
@@ -9494,7 +6649,7 @@ class SDKManager {
|
|
|
9494
6649
|
const user = await sdk.getCurrentUser();
|
|
9495
6650
|
const canPoll = user && hasAnyRole(user.roles, ['ROLE_MERCHANT', 'ROLE_CASHIER']);
|
|
9496
6651
|
if (canPoll) {
|
|
9497
|
-
log
|
|
6652
|
+
log.info('Certificate installed — starting polling');
|
|
9498
6653
|
this.notificationService?.startPolling();
|
|
9499
6654
|
await this.startTelemetryPollingAuto();
|
|
9500
6655
|
this.isPollingActive = true;
|
|
@@ -9507,7 +6662,7 @@ class SDKManager {
|
|
|
9507
6662
|
this.certificateMissingSubject.next(true);
|
|
9508
6663
|
// Stop polling since certificate is required
|
|
9509
6664
|
if (this.isPollingActive) {
|
|
9510
|
-
log
|
|
6665
|
+
log.info('Certificate removed — stopping polling');
|
|
9511
6666
|
this.notificationService?.stopPolling();
|
|
9512
6667
|
this.telemetryService?.stopPolling();
|
|
9513
6668
|
this.telemetryService?.clearTelemetry();
|
|
@@ -9587,268 +6742,5 @@ const RECEIPT_SENT = 'sent';
|
|
|
9587
6742
|
const RECEIPT_SORT_DESCENDING = 'descending';
|
|
9588
6743
|
const RECEIPT_SORT_ASCENDING = 'ascending';
|
|
9589
6744
|
|
|
9590
|
-
|
|
9591
|
-
class CacheHandler {
|
|
9592
|
-
constructor(cache, networkMonitor) {
|
|
9593
|
-
this.cache = cache;
|
|
9594
|
-
this.networkMonitor = networkMonitor;
|
|
9595
|
-
this.currentOnlineState = true;
|
|
9596
|
-
this.setupNetworkMonitoring();
|
|
9597
|
-
}
|
|
9598
|
-
setupNetworkMonitoring() {
|
|
9599
|
-
if (this.networkMonitor) {
|
|
9600
|
-
this.networkSubscription = this.networkMonitor.online$.subscribe((online) => {
|
|
9601
|
-
this.currentOnlineState = online;
|
|
9602
|
-
});
|
|
9603
|
-
}
|
|
9604
|
-
}
|
|
9605
|
-
isOnline() {
|
|
9606
|
-
if (this.networkMonitor) {
|
|
9607
|
-
return this.currentOnlineState;
|
|
9608
|
-
}
|
|
9609
|
-
if (typeof navigator !== 'undefined' && 'onLine' in navigator) {
|
|
9610
|
-
return navigator.onLine;
|
|
9611
|
-
}
|
|
9612
|
-
return false;
|
|
9613
|
-
}
|
|
9614
|
-
async handleCachedRequest(url, requestFn, config) {
|
|
9615
|
-
if (!this.cache || config?.useCache === false) {
|
|
9616
|
-
const response = await requestFn();
|
|
9617
|
-
return response.data;
|
|
9618
|
-
}
|
|
9619
|
-
const cacheKey = this.generateCacheKey(url);
|
|
9620
|
-
const online = this.isOnline();
|
|
9621
|
-
log.debug('Request:', { url, cacheKey, online });
|
|
9622
|
-
if (online) {
|
|
9623
|
-
try {
|
|
9624
|
-
const response = await requestFn();
|
|
9625
|
-
if (this.cache) {
|
|
9626
|
-
await this.cache.set(cacheKey, response.data).catch((error) => {
|
|
9627
|
-
log.error('Failed to cache:', error instanceof Error ? error.message : error);
|
|
9628
|
-
});
|
|
9629
|
-
}
|
|
9630
|
-
return response.data;
|
|
9631
|
-
}
|
|
9632
|
-
catch (error) {
|
|
9633
|
-
const cached = await this.cache.get(cacheKey).catch(() => null);
|
|
9634
|
-
if (cached) {
|
|
9635
|
-
log.debug('Network failed, using cache fallback');
|
|
9636
|
-
return cached.data;
|
|
9637
|
-
}
|
|
9638
|
-
throw error;
|
|
9639
|
-
}
|
|
9640
|
-
}
|
|
9641
|
-
else {
|
|
9642
|
-
const cached = await this.cache.get(cacheKey).catch(() => null);
|
|
9643
|
-
if (cached) {
|
|
9644
|
-
log.debug('Offline, returning cached data');
|
|
9645
|
-
return cached.data;
|
|
9646
|
-
}
|
|
9647
|
-
throw new Error('Offline: No cached data available');
|
|
9648
|
-
}
|
|
9649
|
-
}
|
|
9650
|
-
generateCacheKey(url) {
|
|
9651
|
-
return url;
|
|
9652
|
-
}
|
|
9653
|
-
async invalidateCache(pattern) {
|
|
9654
|
-
if (!this.cache)
|
|
9655
|
-
return;
|
|
9656
|
-
try {
|
|
9657
|
-
await this.cache.invalidate(pattern);
|
|
9658
|
-
}
|
|
9659
|
-
catch (error) {
|
|
9660
|
-
log.error('Invalidation failed:', error instanceof Error ? error.message : error);
|
|
9661
|
-
}
|
|
9662
|
-
}
|
|
9663
|
-
getCacheStatus() {
|
|
9664
|
-
return {
|
|
9665
|
-
available: !!this.cache,
|
|
9666
|
-
networkMonitorAvailable: !!this.networkMonitor,
|
|
9667
|
-
isOnline: this.isOnline(),
|
|
9668
|
-
};
|
|
9669
|
-
}
|
|
9670
|
-
destroy() {
|
|
9671
|
-
this.networkSubscription?.unsubscribe();
|
|
9672
|
-
}
|
|
9673
|
-
}
|
|
9674
|
-
|
|
9675
|
-
function transformError(error) {
|
|
9676
|
-
if (axios.isAxiosError(error)) {
|
|
9677
|
-
const response = error.response;
|
|
9678
|
-
if (!response) {
|
|
9679
|
-
return new ACubeSDKError('NETWORK_ERROR', 'Network error occurred', error);
|
|
9680
|
-
}
|
|
9681
|
-
const status = response.status;
|
|
9682
|
-
const data = response.data;
|
|
9683
|
-
const violations = data?.violations;
|
|
9684
|
-
let message = 'Unknown error occurred';
|
|
9685
|
-
if (data?.detail) {
|
|
9686
|
-
message = data.detail;
|
|
9687
|
-
}
|
|
9688
|
-
else if (data?.title) {
|
|
9689
|
-
message = data.title;
|
|
9690
|
-
}
|
|
9691
|
-
else if (error.message) {
|
|
9692
|
-
message = error.message;
|
|
9693
|
-
}
|
|
9694
|
-
switch (status) {
|
|
9695
|
-
case 400:
|
|
9696
|
-
return new ACubeSDKError('VALIDATION_ERROR', message, error, status, violations);
|
|
9697
|
-
case 401:
|
|
9698
|
-
return new ACubeSDKError('AUTH_ERROR', message, error, status, violations);
|
|
9699
|
-
case 403:
|
|
9700
|
-
return new ACubeSDKError('FORBIDDEN_ERROR', message, error, status, violations);
|
|
9701
|
-
case 404:
|
|
9702
|
-
return new ACubeSDKError('NOT_FOUND_ERROR', message, error, status, violations);
|
|
9703
|
-
case 422:
|
|
9704
|
-
return new ACubeSDKError('VALIDATION_ERROR', message, error, status, violations);
|
|
9705
|
-
default:
|
|
9706
|
-
return new ACubeSDKError('UNKNOWN_ERROR', message, error, status, violations);
|
|
9707
|
-
}
|
|
9708
|
-
}
|
|
9709
|
-
return new ACubeSDKError('UNKNOWN_ERROR', 'Unknown error occurred', error);
|
|
9710
|
-
}
|
|
9711
|
-
|
|
9712
|
-
var ErrorCategory;
|
|
9713
|
-
(function (ErrorCategory) {
|
|
9714
|
-
ErrorCategory["SERVER_ERROR"] = "SERVER_ERROR";
|
|
9715
|
-
ErrorCategory["CLIENT_ERROR"] = "CLIENT_ERROR";
|
|
9716
|
-
ErrorCategory["AUTH_ERROR"] = "AUTH_ERROR";
|
|
9717
|
-
ErrorCategory["CERTIFICATE_ERROR"] = "CERTIFICATE_ERROR";
|
|
9718
|
-
ErrorCategory["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
9719
|
-
ErrorCategory["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
9720
|
-
})(ErrorCategory || (ErrorCategory = {}));
|
|
9721
|
-
function extractStatusCode(error) {
|
|
9722
|
-
if (error instanceof MTLSError && error.statusCode) {
|
|
9723
|
-
return error.statusCode;
|
|
9724
|
-
}
|
|
9725
|
-
const errorObj = error;
|
|
9726
|
-
if (errorObj?.response?.status) {
|
|
9727
|
-
return errorObj.response.status;
|
|
9728
|
-
}
|
|
9729
|
-
if (typeof errorObj?.statusCode === 'number') {
|
|
9730
|
-
return errorObj.statusCode;
|
|
9731
|
-
}
|
|
9732
|
-
return undefined;
|
|
9733
|
-
}
|
|
9734
|
-
function classifyError(error) {
|
|
9735
|
-
const statusCode = extractStatusCode(error);
|
|
9736
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
9737
|
-
if (statusCode && statusCode >= 500 && statusCode < 600) {
|
|
9738
|
-
return {
|
|
9739
|
-
category: ErrorCategory.SERVER_ERROR,
|
|
9740
|
-
statusCode,
|
|
9741
|
-
message: errorMessage,
|
|
9742
|
-
shouldRetry: false,
|
|
9743
|
-
userMessage: `Server error (${statusCode}): The server encountered an error.`,
|
|
9744
|
-
};
|
|
9745
|
-
}
|
|
9746
|
-
if (statusCode === 401 || statusCode === 403) {
|
|
9747
|
-
return {
|
|
9748
|
-
category: ErrorCategory.AUTH_ERROR,
|
|
9749
|
-
statusCode,
|
|
9750
|
-
message: errorMessage,
|
|
9751
|
-
shouldRetry: false,
|
|
9752
|
-
userMessage: `Authentication error (${statusCode}): Invalid credentials or insufficient permissions.`,
|
|
9753
|
-
};
|
|
9754
|
-
}
|
|
9755
|
-
if (statusCode && statusCode >= 400 && statusCode < 500) {
|
|
9756
|
-
return {
|
|
9757
|
-
category: ErrorCategory.CLIENT_ERROR,
|
|
9758
|
-
statusCode,
|
|
9759
|
-
message: errorMessage,
|
|
9760
|
-
shouldRetry: false,
|
|
9761
|
-
userMessage: `Request error (${statusCode}): ${errorMessage}`,
|
|
9762
|
-
};
|
|
9763
|
-
}
|
|
9764
|
-
if (error instanceof MTLSError) {
|
|
9765
|
-
return {
|
|
9766
|
-
category: ErrorCategory.CERTIFICATE_ERROR,
|
|
9767
|
-
statusCode,
|
|
9768
|
-
message: errorMessage,
|
|
9769
|
-
shouldRetry: true,
|
|
9770
|
-
userMessage: 'Certificate error: Unable to establish secure connection.',
|
|
9771
|
-
};
|
|
9772
|
-
}
|
|
9773
|
-
if (!statusCode &&
|
|
9774
|
-
(errorMessage.toLowerCase().includes('network') ||
|
|
9775
|
-
errorMessage.toLowerCase().includes('timeout') ||
|
|
9776
|
-
errorMessage.toLowerCase().includes('connection'))) {
|
|
9777
|
-
return {
|
|
9778
|
-
category: ErrorCategory.NETWORK_ERROR,
|
|
9779
|
-
message: errorMessage,
|
|
9780
|
-
shouldRetry: true,
|
|
9781
|
-
userMessage: 'Network error: Unable to connect to server.',
|
|
9782
|
-
};
|
|
9783
|
-
}
|
|
9784
|
-
return {
|
|
9785
|
-
category: ErrorCategory.UNKNOWN_ERROR,
|
|
9786
|
-
statusCode,
|
|
9787
|
-
message: errorMessage,
|
|
9788
|
-
shouldRetry: false,
|
|
9789
|
-
userMessage: `Unexpected error: ${errorMessage}`,
|
|
9790
|
-
};
|
|
9791
|
-
}
|
|
9792
|
-
function shouldReconfigureCertificate(error) {
|
|
9793
|
-
const classification = classifyError(error);
|
|
9794
|
-
return classification.category === ErrorCategory.CERTIFICATE_ERROR;
|
|
9795
|
-
}
|
|
9796
|
-
function shouldRetryRequest(error, isRetryAttempt) {
|
|
9797
|
-
if (isRetryAttempt) {
|
|
9798
|
-
return false;
|
|
9799
|
-
}
|
|
9800
|
-
const classification = classifyError(error);
|
|
9801
|
-
return classification.shouldRetry;
|
|
9802
|
-
}
|
|
9803
|
-
function getUserFriendlyMessage(error) {
|
|
9804
|
-
const classification = classifyError(error);
|
|
9805
|
-
return classification.userMessage;
|
|
9806
|
-
}
|
|
9807
|
-
|
|
9808
|
-
var MTLSErrorType;
|
|
9809
|
-
(function (MTLSErrorType) {
|
|
9810
|
-
MTLSErrorType["NOT_SUPPORTED"] = "MTLS_NOT_SUPPORTED";
|
|
9811
|
-
MTLSErrorType["CERTIFICATE_NOT_FOUND"] = "MTLS_CERTIFICATE_NOT_FOUND";
|
|
9812
|
-
MTLSErrorType["CERTIFICATE_EXPIRED"] = "MTLS_CERTIFICATE_EXPIRED";
|
|
9813
|
-
MTLSErrorType["CERTIFICATE_INVALID"] = "MTLS_CERTIFICATE_INVALID";
|
|
9814
|
-
MTLSErrorType["CONNECTION_FAILED"] = "MTLS_CONNECTION_FAILED";
|
|
9815
|
-
MTLSErrorType["AUTHENTICATION_FAILED"] = "MTLS_AUTHENTICATION_FAILED";
|
|
9816
|
-
MTLSErrorType["CONFIGURATION_ERROR"] = "MTLS_CONFIGURATION_ERROR";
|
|
9817
|
-
})(MTLSErrorType || (MTLSErrorType = {}));
|
|
9818
|
-
class PlatformDetector {
|
|
9819
|
-
static detectPlatform() {
|
|
9820
|
-
if ((typeof global !== 'undefined' && global.__expo) ||
|
|
9821
|
-
(typeof navigator !== 'undefined' &&
|
|
9822
|
-
navigator.product === 'ReactNative')) {
|
|
9823
|
-
return 'react-native';
|
|
9824
|
-
}
|
|
9825
|
-
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
9826
|
-
return 'node';
|
|
9827
|
-
}
|
|
9828
|
-
return 'web';
|
|
9829
|
-
}
|
|
9830
|
-
static isReactNative() {
|
|
9831
|
-
return this.detectPlatform() === 'react-native';
|
|
9832
|
-
}
|
|
9833
|
-
static isNode() {
|
|
9834
|
-
return this.detectPlatform() === 'node';
|
|
9835
|
-
}
|
|
9836
|
-
static isWeb() {
|
|
9837
|
-
return this.detectPlatform() === 'web';
|
|
9838
|
-
}
|
|
9839
|
-
static getPlatformDetails() {
|
|
9840
|
-
const platform = this.detectPlatform();
|
|
9841
|
-
return {
|
|
9842
|
-
platform,
|
|
9843
|
-
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
|
|
9844
|
-
nodeVersion: typeof process !== 'undefined' ? process.version : undefined,
|
|
9845
|
-
isExpo: typeof global !== 'undefined' && !!global.__expo,
|
|
9846
|
-
hasWindow: typeof window !== 'undefined',
|
|
9847
|
-
hasDocument: typeof document !== 'undefined',
|
|
9848
|
-
hasProcess: typeof process !== 'undefined',
|
|
9849
|
-
};
|
|
9850
|
-
}
|
|
9851
|
-
}
|
|
9852
|
-
|
|
9853
|
-
export { ACubeSDK, ACubeSDKError, ActivationRequestSchema, AddressMapper, AddressSchema, AppStateService, AuthStrategy, AuthenticationService, AxiosHttpAdapter, CacheHandler, CashRegisterCreateSchema, CashRegisterMapper, CashRegisterRepositoryImpl, CashierCreateInputSchema, CashierMapper, CashierRepositoryImpl, CertificateService, CertificateValidator, ConfigManager, DAILY_REPORT_STATUS_OPTIONS, DEFAULT_QUEUE_CONFIG, DIContainer, DI_TOKENS, DailyReportMapper, DailyReportRepositoryImpl, DailyReportStatusSchema, DailyReportsParamsSchema, EXEMPT_VAT_CODES, ErrorCategory, GOOD_OR_SERVICE_OPTIONS, GoodOrServiceSchema, JournalCloseInputSchema, JournalMapper, JournalRepositoryImpl, JwtAuthHandler, LotterySchema, LotterySecretRequestSchema, MTLSError, MTLSErrorType$1 as MTLSErrorType, MerchantCreateInputSchema, MerchantMapper, MerchantRepositoryImpl, MerchantUpdateInputSchema, MessageSchema, MtlsAuthHandler, NOTIFICATION_CODES, NOTIFICATION_LEVELS, NOTIFICATION_SCOPES, NOTIFICATION_SOURCES, NOTIFICATION_TYPES, NotificationDataBlockAtSchema, NotificationDataPemStatusSchema, NotificationListResponseSchema, NotificationMapper, NotificationMf2UnreachableSchema, NotificationPemBackOnlineSchema, NotificationPemsBlockedSchema, NotificationRepositoryImpl, NotificationSchema, NotificationScopeSchema, NotificationService, OfflineManager, OperationQueue, PEMStatusOfflineRequestSchema, PEMStatusSchema, PEM_STATUS_OPTIONS, PEM_TYPE_OPTIONS, PemCreateInputSchema, PemDataSchema, PemMapper, PemRepositoryImpl, PemStatusSchema, PendingReceiptsSchema, PlatformDetector, PointOfSaleMapper, PointOfSaleRepositoryImpl, RECEIPT_PROOF_TYPE_OPTIONS, RECEIPT_READY, RECEIPT_SENT, RECEIPT_SORT_ASCENDING, RECEIPT_SORT_DESCENDING, ReceiptInputSchema, ReceiptItemSchema, ReceiptMapper, ReceiptProofTypeSchema, ReceiptRepositoryImpl, ReceiptReturnInputSchema, ReceiptReturnItemSchema, ReceiptReturnOrVoidViaPEMInputSchema, ReceiptReturnOrVoidWithProofInputSchema, SDKFactory, SDKManager, STANDARD_VAT_RATES, SupplierCreateInputSchema, SupplierMapper, SupplierRepositoryImpl, SupplierUpdateInputSchema, SyncManager, TelemetryMapper, TelemetryMerchantSchema, TelemetryRepositoryImpl, TelemetrySchema, TelemetryService, TelemetrySoftwareSchema, TelemetrySoftwareVersionSchema, TelemetrySupplierSchema, TransmissionSchema, VAT_RATE_CODES, VAT_RATE_CODE_OPTIONS, ValidationMessages, VatRateCodeSchema, VoidReceiptInputSchema, classifyError, clearObject, clearObjectShallow, createACubeMTLSConfig, createACubeSDK, createPrefixedLogger, createACubeSDK as default, detectPlatform, extractRoles, formatDecimal, getUserFriendlyMessage, hasAnyRole, hasNonEmptyValues, hasRole, isEmpty, isTokenExpired, loadPlatformAdapters, logger, parseJwt, shouldReconfigureCertificate, shouldRetryRequest, transformError, validateInput };
|
|
6745
|
+
export { ACubeSDK, ACubeSDKError, AddressMapper, AppStateService, AuthenticationService, CashRegisterMapper, CashierMapper, CertificateService, CertificateValidator, ConfigManager, DAILY_REPORT_STATUS_OPTIONS, DailyReportMapper, EXEMPT_VAT_CODES, GOOD_OR_SERVICE_OPTIONS, GoodOrServiceSchema, JournalMapper, MTLSError, MTLSErrorType, MerchantMapper, NOTIFICATION_CODES, NOTIFICATION_LEVELS, NOTIFICATION_SCOPES, NOTIFICATION_SOURCES, NOTIFICATION_TYPES, NotificationMapper, NotificationService, PEM_STATUS_OPTIONS, PEM_TYPE_OPTIONS, PemMapper, PointOfSaleMapper, RECEIPT_PROOF_TYPE_OPTIONS, RECEIPT_READY, RECEIPT_SENT, RECEIPT_SORT_ASCENDING, RECEIPT_SORT_DESCENDING, ReceiptMapper, ReceiptProofTypeSchema, SDKManager, STANDARD_VAT_RATES, SupplierMapper, TelemetryMapper, TelemetryService, VAT_RATE_CODES, VAT_RATE_CODE_OPTIONS, VatRateCodeSchema, createACubeMTLSConfig, createACubeSDK, createPrefixedLogger, createACubeSDK as default, extractRoles, hasAnyRole, hasRole, isTokenExpired, loadPlatformAdapters, logger, parseJwt };
|
|
9854
6746
|
//# sourceMappingURL=index.native.js.map
|