@capsitech/react-utilities 0.1.12 → 0.1.15
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/lib/Utilities/ApiUtility.axios.d.ts +1 -1
- package/lib/Utilities/BrowserInfo.js +2 -8
- package/lib/Utilities/CrossTabApiCoordinator.d.ts +105 -0
- package/lib/Utilities/CrossTabApiCoordinator.js +407 -0
- package/lib/Utilities/index.d.ts +1 -0
- package/lib/Utilities/index.js +1 -0
- package/package.json +9 -9
|
@@ -48,7 +48,7 @@ declare class ApiUtilityBase {
|
|
|
48
48
|
* @param config Configuration object
|
|
49
49
|
*/
|
|
50
50
|
configure(config: IApiUtilityConfig): void;
|
|
51
|
-
getResponse: <T = any>(endpoint: string, params?: any, options?: IAxiosRequestConfigWithoutParams) => Promise<AxiosResponse<T, any>>;
|
|
51
|
+
getResponse: <T = any>(endpoint: string, params?: any, options?: IAxiosRequestConfigWithoutParams) => Promise<AxiosResponse<T, any, {}>>;
|
|
52
52
|
get: <T = IApiResponse>(endpoint: string, params?: any, throwErrorOn401?: boolean, options?: IAxiosRequestConfigWithoutParams) => Promise<T>;
|
|
53
53
|
getResult: <T = any>(endpoint: string, params?: any, throwErrorOn401?: boolean, options?: IAxiosRequestConfigWithoutParams) => Promise<T | null>;
|
|
54
54
|
post: <T = IApiResponse>(endpoint: string, body: any, contentType?: string, options?: IAxiosRequestConfigWithoutParams) => Promise<T>;
|
|
@@ -124,19 +124,13 @@ export const getScreenColorDepth = () => validateAndGetScreenDetail(screen.color
|
|
|
124
124
|
* If it fails validation, it returns null
|
|
125
125
|
* @returns {number | null} validated value of window's innerWidth
|
|
126
126
|
*/
|
|
127
|
-
export const getWindowWidth = () =>
|
|
128
|
-
const pixelRatio = window.devicePixelRatio || 1;
|
|
129
|
-
return validateAndGetScreenDetail(Math.floor(window.innerWidth * pixelRatio));
|
|
130
|
-
};
|
|
127
|
+
export const getWindowWidth = () => validateAndGetScreenDetail(window.outerWidth);
|
|
131
128
|
/**
|
|
132
129
|
* Function that validates the user's window's interior height in pixels, and then returns it.
|
|
133
130
|
* If it fails validation, it returns null
|
|
134
131
|
* @returns {number | null} validated value of window's innerHeight
|
|
135
132
|
*/
|
|
136
|
-
export const getWindowHeight = () =>
|
|
137
|
-
const pixelRatio = window.devicePixelRatio || 1;
|
|
138
|
-
return validateAndGetScreenDetail(Math.floor(window.innerHeight * pixelRatio));
|
|
139
|
-
};
|
|
133
|
+
export const getWindowHeight = () => validateAndGetScreenDetail(window.outerHeight);
|
|
140
134
|
/**
|
|
141
135
|
* The function returns users browser's do not track setting by checking the navigator
|
|
142
136
|
* and window object for the same
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrossTabApiCoordinator - Ensures API calls execute only once across multiple browser tabs
|
|
3
|
+
* Uses Web Locks API for coordination and BroadcastChannel for result sharing
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Options for CrossTabApiCoordinator:
|
|
7
|
+
* - `cacheTTL`: Time in milliseconds to cache results to prevent duplicate calls (default: 1000ms)
|
|
8
|
+
* - `timeout`: Time in milliseconds before a request times out (default: 30000ms)
|
|
9
|
+
*/
|
|
10
|
+
interface CrossTabApiCoordinatorOptions {
|
|
11
|
+
cacheTTL?: number;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The `CrossTabApiCoordinator` uses modern browser APIs to coordinate API calls:
|
|
16
|
+
* - **Web Locks API**: Ensures only one tab executes the call
|
|
17
|
+
* - **BroadcastChannel API**: Shares results with other tabs
|
|
18
|
+
* - **Built-in caching**: Prevents duplicate calls within a time window
|
|
19
|
+
* - **Fallback**: Automatically falls back to direct API calls if browser APIs aren't supported.
|
|
20
|
+
*/
|
|
21
|
+
declare class CrossTabApiCoordinator {
|
|
22
|
+
private channels;
|
|
23
|
+
private pendingRequests;
|
|
24
|
+
private cache;
|
|
25
|
+
private readonly CACHE_TTL;
|
|
26
|
+
private readonly REQUEST_TIMEOUT;
|
|
27
|
+
private readonly MAX_CACHE_SIZE;
|
|
28
|
+
private cleanupInterval;
|
|
29
|
+
private isSupported;
|
|
30
|
+
constructor();
|
|
31
|
+
/**
|
|
32
|
+
* Check if required browser APIs are supported
|
|
33
|
+
*/
|
|
34
|
+
private checkBrowserSupport;
|
|
35
|
+
/**
|
|
36
|
+
* Start periodic cache cleanup to prevent memory leaks
|
|
37
|
+
*/
|
|
38
|
+
private startPeriodicCleanup;
|
|
39
|
+
/**
|
|
40
|
+
* Remove stale cache entries
|
|
41
|
+
*/
|
|
42
|
+
private cleanupStaleCache;
|
|
43
|
+
/**
|
|
44
|
+
* Remove stale pending requests
|
|
45
|
+
*/
|
|
46
|
+
private cleanupStalePendingRequests;
|
|
47
|
+
/**
|
|
48
|
+
* Validate broadcast message structure
|
|
49
|
+
*/
|
|
50
|
+
private isValidMessage;
|
|
51
|
+
/**
|
|
52
|
+
* Serialize data for broadcast (handle circular references and non-cloneable objects)
|
|
53
|
+
*/
|
|
54
|
+
private serializeData;
|
|
55
|
+
/**
|
|
56
|
+
* Execute an API call coordinated across tabs
|
|
57
|
+
* @param key Unique identifier for this API call (e.g., 'phoneCall-get-123')
|
|
58
|
+
* @param apiCall Function that returns a Promise with the API call
|
|
59
|
+
* @param options Configuration options
|
|
60
|
+
*/
|
|
61
|
+
execute<T>(key: string, apiCall: () => Promise<T>, options?: CrossTabApiCoordinatorOptions): Promise<T>;
|
|
62
|
+
/**
|
|
63
|
+
* Handle messages from other tabs
|
|
64
|
+
*/
|
|
65
|
+
private handleMessage;
|
|
66
|
+
/**
|
|
67
|
+
* Resolve all pending requests for a key
|
|
68
|
+
*/
|
|
69
|
+
private resolvePendingRequests;
|
|
70
|
+
/**
|
|
71
|
+
* Reject all pending requests for a key
|
|
72
|
+
*/
|
|
73
|
+
private rejectPendingRequests;
|
|
74
|
+
/**
|
|
75
|
+
* Cleanup a specific request
|
|
76
|
+
*/
|
|
77
|
+
private cleanupRequest;
|
|
78
|
+
/**
|
|
79
|
+
* Cleanup broadcast channel
|
|
80
|
+
*/
|
|
81
|
+
private cleanupChannel;
|
|
82
|
+
/**
|
|
83
|
+
* Clear cache for a specific key or all keys
|
|
84
|
+
*/
|
|
85
|
+
clearCache(key?: string): void;
|
|
86
|
+
/**
|
|
87
|
+
* Cleanup all resources
|
|
88
|
+
*/
|
|
89
|
+
cleanup(): void;
|
|
90
|
+
}
|
|
91
|
+
export declare const CrossTabApi: CrossTabApiCoordinator;
|
|
92
|
+
/**
|
|
93
|
+
* Helper function to wrap API calls with cross-tab coordination
|
|
94
|
+
* @example
|
|
95
|
+
* const result = await CoordinateCrossTabApiCall(
|
|
96
|
+
* 'phoneCall-get-' + callId,
|
|
97
|
+
* () => PhoneCallService.get(callId, true)
|
|
98
|
+
* );
|
|
99
|
+
*
|
|
100
|
+
* @param key (string): Unique identifier for this API call. Same key = shared execution.
|
|
101
|
+
* @param apiCall (() => Promise<T>): Function that returns the API call promise.
|
|
102
|
+
* @param options (optional)
|
|
103
|
+
*/
|
|
104
|
+
export declare function CoordinateCrossTabApiCall<T>(key: string, apiCall: () => Promise<T>, options?: CrossTabApiCoordinatorOptions): Promise<T>;
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrossTabApiCoordinator - Ensures API calls execute only once across multiple browser tabs
|
|
3
|
+
* Uses Web Locks API for coordination and BroadcastChannel for result sharing
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The `CrossTabApiCoordinator` uses modern browser APIs to coordinate API calls:
|
|
7
|
+
* - **Web Locks API**: Ensures only one tab executes the call
|
|
8
|
+
* - **BroadcastChannel API**: Shares results with other tabs
|
|
9
|
+
* - **Built-in caching**: Prevents duplicate calls within a time window
|
|
10
|
+
* - **Fallback**: Automatically falls back to direct API calls if browser APIs aren't supported.
|
|
11
|
+
*/
|
|
12
|
+
class CrossTabApiCoordinator {
|
|
13
|
+
channels = new Map();
|
|
14
|
+
pendingRequests = new Map();
|
|
15
|
+
cache = new Map();
|
|
16
|
+
CACHE_TTL = 1000; // 1 second cache to avoid duplicate calls
|
|
17
|
+
REQUEST_TIMEOUT = 30000; // 30 seconds timeout
|
|
18
|
+
MAX_CACHE_SIZE = 100; // Prevent memory leaks
|
|
19
|
+
cleanupInterval = null;
|
|
20
|
+
isSupported = null;
|
|
21
|
+
constructor() {
|
|
22
|
+
// Start periodic cleanup
|
|
23
|
+
this.startPeriodicCleanup();
|
|
24
|
+
// Cleanup on page unload
|
|
25
|
+
if (typeof window !== 'undefined') {
|
|
26
|
+
window.addEventListener('beforeunload', () => this.cleanup());
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if required browser APIs are supported
|
|
31
|
+
*/
|
|
32
|
+
checkBrowserSupport() {
|
|
33
|
+
if (this.isSupported !== null) {
|
|
34
|
+
return this.isSupported;
|
|
35
|
+
}
|
|
36
|
+
const hasLocks = typeof navigator !== 'undefined' && 'locks' in navigator;
|
|
37
|
+
const hasBroadcastChannel = typeof BroadcastChannel !== 'undefined';
|
|
38
|
+
this.isSupported = hasLocks && hasBroadcastChannel;
|
|
39
|
+
if (!this.isSupported) {
|
|
40
|
+
console.warn('CrossTabApiCoordinator: Required APIs not supported. Missing:', !hasLocks ? 'Web Locks API' : '', !hasBroadcastChannel ? 'BroadcastChannel API' : '');
|
|
41
|
+
}
|
|
42
|
+
return this.isSupported;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Start periodic cache cleanup to prevent memory leaks
|
|
46
|
+
*/
|
|
47
|
+
startPeriodicCleanup() {
|
|
48
|
+
if (this.cleanupInterval)
|
|
49
|
+
return;
|
|
50
|
+
this.cleanupInterval = setInterval(() => {
|
|
51
|
+
this.cleanupStaleCache();
|
|
52
|
+
this.cleanupStalePendingRequests();
|
|
53
|
+
}, 60000); // Run every minute
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Remove stale cache entries
|
|
57
|
+
*/
|
|
58
|
+
cleanupStaleCache() {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const maxAge = 300000; // 5 minutes
|
|
61
|
+
const entries = Array.from(this.cache.entries());
|
|
62
|
+
for (const [key, value] of entries) {
|
|
63
|
+
if (now - value.timestamp > maxAge) {
|
|
64
|
+
this.cache.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// If cache is still too large, remove oldest entries
|
|
68
|
+
if (this.cache.size > this.MAX_CACHE_SIZE) {
|
|
69
|
+
const entries = Array.from(this.cache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
70
|
+
const toRemove = entries.slice(0, entries.length - this.MAX_CACHE_SIZE);
|
|
71
|
+
toRemove.forEach(([key]) => this.cache.delete(key));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Remove stale pending requests
|
|
76
|
+
*/
|
|
77
|
+
cleanupStalePendingRequests() {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const maxAge = this.REQUEST_TIMEOUT * 2;
|
|
80
|
+
const entries = Array.from(this.pendingRequests.entries());
|
|
81
|
+
for (const [key, requests] of entries) {
|
|
82
|
+
const validRequests = requests.filter((req) => {
|
|
83
|
+
if (now - req.timestamp > maxAge) {
|
|
84
|
+
// Clear timeout and reject
|
|
85
|
+
clearTimeout(req.timeoutId);
|
|
86
|
+
req.reject(new Error('Request timed out and cleaned up'));
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
if (validRequests.length === 0) {
|
|
92
|
+
this.pendingRequests.delete(key);
|
|
93
|
+
this.cleanupChannel(key);
|
|
94
|
+
}
|
|
95
|
+
else if (validRequests.length !== requests.length) {
|
|
96
|
+
this.pendingRequests.set(key, validRequests);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Validate broadcast message structure
|
|
102
|
+
*/
|
|
103
|
+
isValidMessage(data) {
|
|
104
|
+
return (data && typeof data === 'object' && typeof data.type === 'string' && (data.type === 'success' || data.type === 'error') && typeof data.key === 'string' && typeof data.timestamp === 'number');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Serialize data for broadcast (handle circular references and non-cloneable objects)
|
|
108
|
+
*/
|
|
109
|
+
serializeData(data) {
|
|
110
|
+
try {
|
|
111
|
+
// Test if data is cloneable by attempting to structure clone
|
|
112
|
+
structuredClone(data);
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// Fallback to JSON serialization for non-cloneable data
|
|
117
|
+
console.warn('Data contains non-cloneable objects, using JSON serialization');
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(JSON.stringify(data));
|
|
120
|
+
}
|
|
121
|
+
catch (jsonError) {
|
|
122
|
+
console.error('Failed to serialize data:', jsonError);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Execute an API call coordinated across tabs
|
|
129
|
+
* @param key Unique identifier for this API call (e.g., 'phoneCall-get-123')
|
|
130
|
+
* @param apiCall Function that returns a Promise with the API call
|
|
131
|
+
* @param options Configuration options
|
|
132
|
+
*/
|
|
133
|
+
async execute(key, apiCall, options = {}) {
|
|
134
|
+
const cacheTTL = options.cacheTTL ?? this.CACHE_TTL;
|
|
135
|
+
const timeout = options.timeout ?? this.REQUEST_TIMEOUT;
|
|
136
|
+
// Validate key
|
|
137
|
+
if (!key || typeof key !== 'string') {
|
|
138
|
+
throw new Error('Invalid key: must be a non-empty string');
|
|
139
|
+
}
|
|
140
|
+
// Check cache first
|
|
141
|
+
const cached = this.cache.get(key);
|
|
142
|
+
if (cached && Date.now() - cached.timestamp < cacheTTL) {
|
|
143
|
+
return cached.data;
|
|
144
|
+
}
|
|
145
|
+
// Check if required APIs are supported - fallback to direct execution
|
|
146
|
+
if (!this.checkBrowserSupport()) {
|
|
147
|
+
console.warn(`CrossTabApiCoordinator: Browser APIs not supported, executing API call directly for key: ${key}`);
|
|
148
|
+
return apiCall();
|
|
149
|
+
}
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
const requestId = `${key}-${Date.now()}-${Math.random()}`;
|
|
152
|
+
const channelName = `api-coordinator-${key}`;
|
|
153
|
+
let isResolved = false;
|
|
154
|
+
// Create timeout with cleanup
|
|
155
|
+
const timeoutId = setTimeout(() => {
|
|
156
|
+
if (!isResolved) {
|
|
157
|
+
isResolved = true;
|
|
158
|
+
this.cleanupRequest(key, requestId);
|
|
159
|
+
reject(new Error(`API call timeout for key: ${key}`));
|
|
160
|
+
}
|
|
161
|
+
}, timeout);
|
|
162
|
+
// Store this request
|
|
163
|
+
if (!this.pendingRequests.has(key)) {
|
|
164
|
+
this.pendingRequests.set(key, []);
|
|
165
|
+
}
|
|
166
|
+
this.pendingRequests.get(key).push({
|
|
167
|
+
resolve: (value) => {
|
|
168
|
+
if (!isResolved) {
|
|
169
|
+
isResolved = true;
|
|
170
|
+
clearTimeout(timeoutId);
|
|
171
|
+
resolve(value);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
reject: (error) => {
|
|
175
|
+
if (!isResolved) {
|
|
176
|
+
isResolved = true;
|
|
177
|
+
clearTimeout(timeoutId);
|
|
178
|
+
reject(error);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
timeoutId,
|
|
183
|
+
});
|
|
184
|
+
// Get or create broadcast channel for this key
|
|
185
|
+
// CRITICAL: Must be created BEFORE attempting lock to avoid missing broadcasts
|
|
186
|
+
let channel;
|
|
187
|
+
try {
|
|
188
|
+
if (!this.channels.has(key)) {
|
|
189
|
+
channel = new BroadcastChannel(channelName);
|
|
190
|
+
channel.onmessage = (event) => this.handleMessage(key, event);
|
|
191
|
+
channel.onmessageerror = (event) => {
|
|
192
|
+
console.error('BroadcastChannel message error:', event);
|
|
193
|
+
};
|
|
194
|
+
this.channels.set(key, channel);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
channel = this.channels.get(key);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.error('Failed to create BroadcastChannel:', error);
|
|
202
|
+
console.warn('Falling back to direct API execution without coordination');
|
|
203
|
+
clearTimeout(timeoutId);
|
|
204
|
+
this.cleanupRequest(key, requestId);
|
|
205
|
+
// Fallback to direct execution if BroadcastChannel fails
|
|
206
|
+
return apiCall().then(resolve).catch(reject);
|
|
207
|
+
}
|
|
208
|
+
// Try to acquire lock with ifAvailable - only first tab gets it immediately
|
|
209
|
+
// Other tabs will fail to get the lock and wait for broadcast
|
|
210
|
+
navigator.locks
|
|
211
|
+
.request(`api-${key}`, { mode: 'exclusive', ifAvailable: true }, async (lock) => {
|
|
212
|
+
if (!lock) {
|
|
213
|
+
// This tab didn't get the lock - another tab is executing
|
|
214
|
+
// Just wait for the broadcast message (already set up via pendingRequests)
|
|
215
|
+
return null; // Return control immediately, wait for broadcast
|
|
216
|
+
}
|
|
217
|
+
// This tab got the lock - execute the API call
|
|
218
|
+
try {
|
|
219
|
+
const result = await apiCall();
|
|
220
|
+
// Serialize data to ensure it can be broadcast
|
|
221
|
+
const serializedResult = this.serializeData(result);
|
|
222
|
+
// Cache the result
|
|
223
|
+
this.cache.set(key, { data: result, timestamp: Date.now() });
|
|
224
|
+
// Broadcast result to all tabs (including this one)
|
|
225
|
+
try {
|
|
226
|
+
const message = {
|
|
227
|
+
type: 'success',
|
|
228
|
+
data: serializedResult,
|
|
229
|
+
key,
|
|
230
|
+
timestamp: Date.now(),
|
|
231
|
+
};
|
|
232
|
+
channel.postMessage(message);
|
|
233
|
+
}
|
|
234
|
+
catch (broadcastError) {
|
|
235
|
+
console.error('Failed to broadcast success:', broadcastError);
|
|
236
|
+
// Still resolve local requests even if broadcast fails
|
|
237
|
+
}
|
|
238
|
+
// Resolve all pending requests in this tab
|
|
239
|
+
this.resolvePendingRequests(key, result);
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
// Broadcast error to all tabs
|
|
244
|
+
try {
|
|
245
|
+
const message = {
|
|
246
|
+
type: 'error',
|
|
247
|
+
error: error instanceof Error ? error.message : String(error),
|
|
248
|
+
key,
|
|
249
|
+
timestamp: Date.now(),
|
|
250
|
+
};
|
|
251
|
+
channel.postMessage(message);
|
|
252
|
+
}
|
|
253
|
+
catch (broadcastError) {
|
|
254
|
+
console.error('Failed to broadcast error:', broadcastError);
|
|
255
|
+
}
|
|
256
|
+
// Reject all pending requests in this tab
|
|
257
|
+
this.rejectPendingRequests(key, error);
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
// Clean up channel after a delay
|
|
262
|
+
setTimeout(() => this.cleanupChannel(key), 5000);
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
.catch((error) => {
|
|
266
|
+
console.error('Lock request failed:', error);
|
|
267
|
+
console.warn('Falling back to direct API execution without coordination');
|
|
268
|
+
clearTimeout(timeoutId);
|
|
269
|
+
this.cleanupRequest(key, requestId);
|
|
270
|
+
// If lock mechanism fails, fall back to direct execution
|
|
271
|
+
if (!isResolved) {
|
|
272
|
+
isResolved = true;
|
|
273
|
+
apiCall().then(resolve).catch(reject);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Handle messages from other tabs
|
|
280
|
+
*/
|
|
281
|
+
handleMessage(key, event) {
|
|
282
|
+
// Validate message structure
|
|
283
|
+
if (!this.isValidMessage(event.data)) {
|
|
284
|
+
console.warn('Received invalid message:', event.data);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const { type, data, error, timestamp } = event.data;
|
|
288
|
+
// Ignore stale messages (older than 1 minute)
|
|
289
|
+
if (Date.now() - timestamp > 60000) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (type === 'success') {
|
|
293
|
+
// Cache the result
|
|
294
|
+
this.cache.set(key, { data, timestamp: Date.now() });
|
|
295
|
+
// Resolve all pending requests
|
|
296
|
+
this.resolvePendingRequests(key, data);
|
|
297
|
+
}
|
|
298
|
+
else if (type === 'error') {
|
|
299
|
+
// Reject all pending requests
|
|
300
|
+
this.rejectPendingRequests(key, new Error(error || 'Unknown error'));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Resolve all pending requests for a key
|
|
305
|
+
*/
|
|
306
|
+
resolvePendingRequests(key, data) {
|
|
307
|
+
const requests = this.pendingRequests.get(key) || [];
|
|
308
|
+
requests.forEach((req) => req.resolve(data));
|
|
309
|
+
this.pendingRequests.delete(key);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Reject all pending requests for a key
|
|
313
|
+
*/
|
|
314
|
+
rejectPendingRequests(key, error) {
|
|
315
|
+
const requests = this.pendingRequests.get(key) || [];
|
|
316
|
+
requests.forEach((req) => req.reject(error));
|
|
317
|
+
this.pendingRequests.delete(key);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Cleanup a specific request
|
|
321
|
+
*/
|
|
322
|
+
cleanupRequest(key, requestId) {
|
|
323
|
+
const requests = this.pendingRequests.get(key);
|
|
324
|
+
if (requests) {
|
|
325
|
+
const filtered = requests.filter((req) => req.timestamp.toString() !== requestId.split('-')[1]);
|
|
326
|
+
if (filtered.length > 0) {
|
|
327
|
+
this.pendingRequests.set(key, filtered);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
this.pendingRequests.delete(key);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Cleanup broadcast channel
|
|
336
|
+
*/
|
|
337
|
+
cleanupChannel(key) {
|
|
338
|
+
const channel = this.channels.get(key);
|
|
339
|
+
if (channel && (!this.pendingRequests.has(key) || this.pendingRequests.get(key).length === 0)) {
|
|
340
|
+
channel.close();
|
|
341
|
+
this.channels.delete(key);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Clear cache for a specific key or all keys
|
|
346
|
+
*/
|
|
347
|
+
clearCache(key) {
|
|
348
|
+
if (key) {
|
|
349
|
+
this.cache.delete(key);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
this.cache.clear();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Cleanup all resources
|
|
357
|
+
*/
|
|
358
|
+
cleanup() {
|
|
359
|
+
// Clear periodic cleanup
|
|
360
|
+
if (this.cleanupInterval) {
|
|
361
|
+
clearInterval(this.cleanupInterval);
|
|
362
|
+
this.cleanupInterval = null;
|
|
363
|
+
}
|
|
364
|
+
// Clear all timeouts in pending requests
|
|
365
|
+
this.pendingRequests.forEach((requests) => {
|
|
366
|
+
requests.forEach((req) => {
|
|
367
|
+
clearTimeout(req.timeoutId);
|
|
368
|
+
// Reject pending requests on cleanup
|
|
369
|
+
try {
|
|
370
|
+
req.reject(new Error('Coordinator cleanup'));
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
// Ignore if already resolved
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
// Close all channels
|
|
378
|
+
this.channels.forEach((channel) => {
|
|
379
|
+
try {
|
|
380
|
+
channel.close();
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
console.error('Error closing channel:', e);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
this.channels.clear();
|
|
387
|
+
this.pendingRequests.clear();
|
|
388
|
+
this.cache.clear();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Export singleton instance
|
|
392
|
+
export const CrossTabApi = new CrossTabApiCoordinator();
|
|
393
|
+
/**
|
|
394
|
+
* Helper function to wrap API calls with cross-tab coordination
|
|
395
|
+
* @example
|
|
396
|
+
* const result = await CoordinateCrossTabApiCall(
|
|
397
|
+
* 'phoneCall-get-' + callId,
|
|
398
|
+
* () => PhoneCallService.get(callId, true)
|
|
399
|
+
* );
|
|
400
|
+
*
|
|
401
|
+
* @param key (string): Unique identifier for this API call. Same key = shared execution.
|
|
402
|
+
* @param apiCall (() => Promise<T>): Function that returns the API call promise.
|
|
403
|
+
* @param options (optional)
|
|
404
|
+
*/
|
|
405
|
+
export async function CoordinateCrossTabApiCall(key, apiCall, options) {
|
|
406
|
+
return CrossTabApi.execute(key, apiCall, options);
|
|
407
|
+
}
|
package/lib/Utilities/index.d.ts
CHANGED
package/lib/Utilities/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capsitech/react-utilities",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "A set of javascript utility methods",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"jsnext:main": "lib/index.js",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"dayjs": "^1.11.19",
|
|
40
|
-
"lodash": "^4.17.
|
|
40
|
+
"lodash": "^4.17.23"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"axios": "^1.13.
|
|
43
|
+
"axios": "^1.13.3",
|
|
44
44
|
"file-saver": "^2.0.5",
|
|
45
|
-
"qs": "^6.14.
|
|
45
|
+
"qs": "^6.14.1",
|
|
46
46
|
"react": ">=18",
|
|
47
47
|
"react-dom": ">=18"
|
|
48
48
|
},
|
|
@@ -54,16 +54,16 @@
|
|
|
54
54
|
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
|
55
55
|
"@storybook/react-webpack5": "^10.1.11",
|
|
56
56
|
"@types/file-saver": "^2.0.7",
|
|
57
|
-
"@types/lodash": "4.17.
|
|
57
|
+
"@types/lodash": "4.17.23",
|
|
58
58
|
"@types/qs": "^6",
|
|
59
59
|
"@types/react": "^18.3.12",
|
|
60
|
-
"axios": "^1.
|
|
60
|
+
"axios": "^1.13.3",
|
|
61
61
|
"file-saver": "^2.0.5",
|
|
62
|
-
"qs": "^6.14.
|
|
62
|
+
"qs": "^6.14.1",
|
|
63
63
|
"react": "^18.3.1",
|
|
64
64
|
"react-dom": "^18.3.1",
|
|
65
65
|
"react-router-dom": "^7.8.1",
|
|
66
|
-
"rimraf": "6.
|
|
66
|
+
"rimraf": "6.1.2",
|
|
67
67
|
"storybook": "^10.1.11",
|
|
68
68
|
"typescript": "5.8.3"
|
|
69
69
|
},
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"last 1 safari version"
|
|
80
80
|
]
|
|
81
81
|
},
|
|
82
|
-
"packageManager": "yarn@4.
|
|
82
|
+
"packageManager": "yarn@4.12.0"
|
|
83
83
|
}
|