request-iframe 0.0.3 → 0.0.5

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.
Files changed (69) hide show
  1. package/QUICKSTART.CN.md +35 -8
  2. package/QUICKSTART.md +35 -8
  3. package/README.CN.md +177 -24
  4. package/README.md +237 -19
  5. package/library/__tests__/channel.test.ts +16 -4
  6. package/library/__tests__/coverage-branches.test.ts +356 -0
  7. package/library/__tests__/debug.test.ts +22 -0
  8. package/library/__tests__/dispatcher.test.ts +8 -4
  9. package/library/__tests__/requestIframe.test.ts +1243 -87
  10. package/library/__tests__/stream.test.ts +92 -16
  11. package/library/__tests__/utils.test.ts +41 -1
  12. package/library/api/client.d.ts.map +1 -1
  13. package/library/api/client.js +1 -0
  14. package/library/constants/index.d.ts +2 -0
  15. package/library/constants/index.d.ts.map +1 -1
  16. package/library/constants/index.js +3 -1
  17. package/library/constants/messages.d.ts +3 -0
  18. package/library/constants/messages.d.ts.map +1 -1
  19. package/library/constants/messages.js +3 -0
  20. package/library/core/client-server.d.ts +4 -0
  21. package/library/core/client-server.d.ts.map +1 -1
  22. package/library/core/client-server.js +45 -22
  23. package/library/core/client.d.ts +36 -4
  24. package/library/core/client.d.ts.map +1 -1
  25. package/library/core/client.js +508 -285
  26. package/library/core/request.d.ts +3 -1
  27. package/library/core/request.d.ts.map +1 -1
  28. package/library/core/request.js +2 -1
  29. package/library/core/response.d.ts +26 -4
  30. package/library/core/response.d.ts.map +1 -1
  31. package/library/core/response.js +192 -112
  32. package/library/core/server.d.ts +13 -0
  33. package/library/core/server.d.ts.map +1 -1
  34. package/library/core/server.js +221 -6
  35. package/library/index.d.ts +2 -1
  36. package/library/index.d.ts.map +1 -1
  37. package/library/index.js +39 -3
  38. package/library/message/channel.d.ts +2 -2
  39. package/library/message/channel.d.ts.map +1 -1
  40. package/library/message/channel.js +5 -1
  41. package/library/message/dispatcher.d.ts +2 -2
  42. package/library/message/dispatcher.d.ts.map +1 -1
  43. package/library/message/dispatcher.js +6 -5
  44. package/library/stream/index.d.ts +11 -1
  45. package/library/stream/index.d.ts.map +1 -1
  46. package/library/stream/index.js +21 -3
  47. package/library/stream/types.d.ts +2 -2
  48. package/library/stream/types.d.ts.map +1 -1
  49. package/library/stream/writable-stream.d.ts +1 -1
  50. package/library/stream/writable-stream.d.ts.map +1 -1
  51. package/library/stream/writable-stream.js +87 -47
  52. package/library/types/index.d.ts +29 -5
  53. package/library/types/index.d.ts.map +1 -1
  54. package/library/utils/debug.d.ts.map +1 -1
  55. package/library/utils/debug.js +6 -2
  56. package/library/utils/error.d.ts +21 -0
  57. package/library/utils/error.d.ts.map +1 -0
  58. package/library/utils/error.js +34 -0
  59. package/library/utils/index.d.ts +21 -0
  60. package/library/utils/index.d.ts.map +1 -1
  61. package/library/utils/index.js +141 -2
  62. package/library/utils/path-match.d.ts +16 -0
  63. package/library/utils/path-match.d.ts.map +1 -1
  64. package/library/utils/path-match.js +65 -0
  65. package/package.json +2 -1
  66. package/react/library/__tests__/index.test.tsx +44 -22
  67. package/react/library/index.d.ts.map +1 -1
  68. package/react/library/index.js +81 -23
  69. package/react/package.json +7 -0
@@ -9,20 +9,32 @@ var _exportNames = {
9
9
  generateInstanceId: true,
10
10
  getIframeTargetOrigin: true,
11
11
  isPromise: true,
12
+ isWindowAvailable: true,
13
+ detectContentType: true,
14
+ blobToBase64: true,
12
15
  createPostMessage: true,
13
16
  isValidPostMessage: true,
14
17
  validatePostMessage: true,
15
18
  validateProtocolVersion: true,
16
19
  isRequestIframeMessage: true,
17
20
  getProtocolVersion: true,
18
- isCompatibleVersion: true
21
+ isCompatibleVersion: true,
22
+ RequestIframeError: true
19
23
  };
24
+ Object.defineProperty(exports, "RequestIframeError", {
25
+ enumerable: true,
26
+ get: function get() {
27
+ return _error.RequestIframeError;
28
+ }
29
+ });
30
+ exports.blobToBase64 = blobToBase64;
20
31
  Object.defineProperty(exports, "createPostMessage", {
21
32
  enumerable: true,
22
33
  get: function get() {
23
34
  return _protocol.createPostMessage;
24
35
  }
25
36
  });
37
+ exports.detectContentType = detectContentType;
26
38
  exports.generateInstanceId = generateInstanceId;
27
39
  exports.generateRequestId = generateRequestId;
28
40
  exports.getIframeTargetOrigin = getIframeTargetOrigin;
@@ -51,6 +63,7 @@ Object.defineProperty(exports, "isValidPostMessage", {
51
63
  return _protocol.isValidPostMessage;
52
64
  }
53
65
  });
66
+ exports.isWindowAvailable = isWindowAvailable;
54
67
  Object.defineProperty(exports, "validatePostMessage", {
55
68
  enumerable: true,
56
69
  get: function get() {
@@ -63,8 +76,11 @@ Object.defineProperty(exports, "validateProtocolVersion", {
63
76
  return _protocol.validateProtocolVersion;
64
77
  }
65
78
  });
79
+ require("core-js/modules/es.array.includes.js");
66
80
  require("core-js/modules/es.array.iterator.js");
81
+ require("core-js/modules/es.promise.js");
67
82
  require("core-js/modules/es.regexp.to-string.js");
83
+ require("core-js/modules/es.string.includes.js");
68
84
  require("core-js/modules/web.dom-collections.iterator.js");
69
85
  require("core-js/modules/web.url.js");
70
86
  require("core-js/modules/web.url.to-json.js");
@@ -106,6 +122,7 @@ Object.keys(_cookie).forEach(function (key) {
106
122
  }
107
123
  });
108
124
  });
125
+ var _error = require("./error");
109
126
  /**
110
127
  * Generate unique request ID
111
128
  */
@@ -136,10 +153,132 @@ function getIframeTargetOrigin(iframe) {
136
153
  function isPromise(value) {
137
154
  return value !== null && typeof value === 'object' && 'then' in value;
138
155
  }
156
+
157
+ /**
158
+ * Check if target window is still available (not closed/removed)
159
+ * @param targetWindow Target window to check
160
+ * @returns true if window is available, false otherwise
161
+ */
162
+ function isWindowAvailable(targetWindow) {
163
+ if (!targetWindow) {
164
+ return false;
165
+ }
166
+ try {
167
+ // Must have postMessage to be a usable target
168
+ if (typeof targetWindow.postMessage !== 'function') {
169
+ return false;
170
+ }
171
+
172
+ // For windows opened via window.open(), check closed property
173
+ if ('closed' in targetWindow && targetWindow.closed === true) {
174
+ return false;
175
+ }
176
+
177
+ // Avoid touching cross-origin properties (like document) which may throw.
178
+ // If closed is not true and postMessage exists, treat as available.
179
+ return true;
180
+ } catch (e) {
181
+ // If accessing window properties throws an error, window is likely closed
182
+ return false;
183
+ }
184
+ }
139
185
  // Export protocol-related functions
140
186
 
141
187
  // Export cache-related functions
142
188
 
143
189
  // Export path matching functions
144
190
 
145
- // Export Cookie-related functions
191
+ // Export Cookie-related functions
192
+
193
+ // Export Error class
194
+
195
+ /**
196
+ * Detect Content-Type based on data type
197
+ * @param data The data to detect Content-Type for
198
+ * @param options Options for detection
199
+ * @param options.checkStream Whether to check for IframeWritableStream (default: false)
200
+ * @param options.isIframeWritableStream Optional function to check if data is a stream (required if checkStream is true)
201
+ * @returns The detected Content-Type, or null if Content-Type should not be auto-set
202
+ */
203
+ function detectContentType(data, options) {
204
+ if (data === null || data === undefined) return null;
205
+ var _ref = options || {},
206
+ _ref$checkStream = _ref.checkStream,
207
+ checkStream = _ref$checkStream === void 0 ? false : _ref$checkStream,
208
+ isIframeWritableStream = _ref.isIframeWritableStream;
209
+
210
+ // Stream - handled separately (only for response)
211
+ if (checkStream && isIframeWritableStream) {
212
+ if (isIframeWritableStream(data)) {
213
+ return null; // Stream will be handled by sendStream
214
+ }
215
+ }
216
+
217
+ // File
218
+ if (typeof File !== 'undefined' && data instanceof File) {
219
+ return data.type || 'application/octet-stream';
220
+ }
221
+
222
+ // Blob
223
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
224
+ return data.type || 'application/octet-stream';
225
+ }
226
+
227
+ // ArrayBuffer
228
+ if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer) {
229
+ return 'application/octet-stream';
230
+ }
231
+
232
+ // FormData
233
+ if (typeof FormData !== 'undefined' && data instanceof FormData) {
234
+ // FormData typically doesn't need Content-Type header (browser sets it with boundary)
235
+ return null;
236
+ }
237
+
238
+ // URLSearchParams
239
+ if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams) {
240
+ return 'application/x-www-form-urlencoded';
241
+ }
242
+
243
+ // String - check if it's JSON string
244
+ if (typeof data === 'string') {
245
+ // Try to parse as JSON, if successful, treat as JSON
246
+ try {
247
+ JSON.parse(data);
248
+ return 'application/json';
249
+ } catch (_unused) {
250
+ return 'text/plain; charset=utf-8';
251
+ }
252
+ }
253
+
254
+ // Number, boolean - treat as JSON
255
+ if (typeof data === 'number' || typeof data === 'boolean') {
256
+ return 'application/json';
257
+ }
258
+
259
+ // Plain object or array - treat as JSON
260
+ if (typeof data === 'object') {
261
+ // Exclude common binary/file types (already checked above, but double-check for safety)
262
+ if (typeof Blob !== 'undefined' && data instanceof Blob) return null;
263
+ if (typeof File !== 'undefined' && data instanceof File) return null;
264
+ if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer) return null;
265
+ if (typeof FormData !== 'undefined' && data instanceof FormData) return null;
266
+ if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams) return null;
267
+ return 'application/json';
268
+ }
269
+ return null;
270
+ }
271
+
272
+ /** Convert Blob to base64 string */
273
+ function blobToBase64(blob) {
274
+ return new Promise((resolve, reject) => {
275
+ var reader = new FileReader();
276
+ reader.onloadend = () => {
277
+ var result = reader.result;
278
+ var base64 = result.includes(',') ? result.split(',')[1] : result;
279
+ resolve(base64);
280
+ };
281
+ reader.onerror = reject;
282
+ reader.readAsDataURL(blob);
283
+ });
284
+ }
@@ -14,4 +14,20 @@ export type PathPattern = string;
14
14
  * @returns whether matches
15
15
  */
16
16
  export declare function matchPath(path: string, matcher: PathMatcher): boolean;
17
+ /**
18
+ * Path match result with extracted parameters
19
+ */
20
+ export interface PathMatchResult {
21
+ /** Whether the path matches */
22
+ match: boolean;
23
+ /** Extracted path parameters (e.g., { id: '123' } for '/api/users/:id' and '/api/users/123') */
24
+ params: Record<string, string>;
25
+ }
26
+ /**
27
+ * Match path with parameter extraction (supports :param syntax like Express)
28
+ * @param path request path
29
+ * @param pattern path pattern with parameters (e.g., '/api/users/:id')
30
+ * @returns match result with extracted parameters
31
+ */
32
+ export declare function matchPathWithParams(path: string, pattern: string): PathMatchResult;
17
33
  //# sourceMappingURL=path-match.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"path-match.d.ts","sourceRoot":"","sources":["../../src/utils/path-match.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAE/F;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CA+CrE"}
1
+ {"version":3,"file":"path-match.d.ts","sourceRoot":"","sources":["../../src/utils/path-match.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAE/F;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAoDrE;AAkBD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,+BAA+B;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,gGAAgG;IAChG,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,CAiDlF"}
@@ -4,12 +4,14 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.matchPath = matchPath;
7
+ exports.matchPathWithParams = matchPathWithParams;
7
8
  require("core-js/modules/es.array.includes.js");
8
9
  require("core-js/modules/es.regexp.constructor.js");
9
10
  require("core-js/modules/es.regexp.exec.js");
10
11
  require("core-js/modules/es.regexp.to-string.js");
11
12
  require("core-js/modules/es.string.ends-with.js");
12
13
  require("core-js/modules/es.string.includes.js");
14
+ require("core-js/modules/es.string.match.js");
13
15
  require("core-js/modules/es.string.replace.js");
14
16
  require("core-js/modules/es.string.starts-with.js");
15
17
  /**
@@ -69,6 +71,11 @@ function matchPath(path, matcher) {
69
71
  if (matcher.includes('*')) {
70
72
  return matchPattern(normalizedPath, normalizedMatcher);
71
73
  }
74
+
75
+ // Support parameter patterns (e.g., '/api/users/:id')
76
+ if (matcher.includes(':')) {
77
+ return matchPathWithParams(normalizedPath, normalizedMatcher).match;
78
+ }
72
79
  return false;
73
80
  }
74
81
  return false;
@@ -87,4 +94,62 @@ function matchPattern(path, pattern) {
87
94
  .replace(/\*/g, '.*'); // Replace * with .*
88
95
  var regex = new RegExp(`^${regexPattern}$`);
89
96
  return regex.test(path);
97
+ }
98
+
99
+ /**
100
+ * Path match result with extracted parameters
101
+ */
102
+
103
+ /**
104
+ * Match path with parameter extraction (supports :param syntax like Express)
105
+ * @param path request path
106
+ * @param pattern path pattern with parameters (e.g., '/api/users/:id')
107
+ * @returns match result with extracted parameters
108
+ */
109
+ function matchPathWithParams(path, pattern) {
110
+ // Normalize paths
111
+ var normalizedPath = path.startsWith('/') ? path : `/${path}`;
112
+ var normalizedPattern = pattern.startsWith('/') ? pattern : `/${pattern}`;
113
+
114
+ // Check if pattern contains parameters (:param)
115
+ if (!normalizedPattern.includes(':')) {
116
+ // No parameters, use exact match
117
+ return {
118
+ match: normalizedPath === normalizedPattern,
119
+ params: {}
120
+ };
121
+ }
122
+
123
+ // Extract parameter names from pattern
124
+ var paramNames = [];
125
+ var paramRegex = /:([^/]+)/g;
126
+ var match;
127
+ while ((match = paramRegex.exec(normalizedPattern)) !== null) {
128
+ paramNames.push(match[1]);
129
+ }
130
+
131
+ // Convert pattern to regex, replacing :param with capture groups
132
+ // '/api/users/:id' -> '^/api/users/([^/]+)$'
133
+ // '/api/users/:id/posts/:postId' -> '^/api/users/([^/]+)/posts/([^/]+)$'
134
+ var regexPattern = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special characters
135
+ .replace(/:[^/]+/g, '([^/]+)'); // Replace :param with capture group
136
+
137
+ var regex = new RegExp(`^${regexPattern}$`);
138
+ var regexMatch = regex.exec(normalizedPath);
139
+ if (!regexMatch) {
140
+ return {
141
+ match: false,
142
+ params: {}
143
+ };
144
+ }
145
+
146
+ // Extract parameter values from match groups
147
+ var params = {};
148
+ for (var i = 0; i < paramNames.length; i++) {
149
+ params[paramNames[i]] = regexMatch[i + 1];
150
+ }
151
+ return {
152
+ match: true,
153
+ params
154
+ };
90
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "request-iframe",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Communicate with iframes like sending HTTP requests",
5
5
  "main": "library/index.js",
6
6
  "types": "library/index.d.ts",
@@ -20,6 +20,7 @@
20
20
  "files": [
21
21
  "library",
22
22
  "react/library",
23
+ "react/package.json",
23
24
  "README.md",
24
25
  "README.CN.md",
25
26
  "QUICKSTART.md",
@@ -221,14 +221,18 @@ describe('React Hooks', () => {
221
221
  configurable: true
222
222
  });
223
223
 
224
- const { result, rerender } = renderHook(() => useClient(() => iframeRef.current));
224
+ type Props = { ver: number };
225
+ const { result, rerender } = renderHook(
226
+ ({ ver }: Props) => useClient(() => iframeRef.current, undefined, [ver]),
227
+ { initialProps: { ver: 0 } as Props }
228
+ );
225
229
 
226
230
  // Initially null (ref not set)
227
231
  expect(result.current).toBeNull();
228
232
 
229
233
  // Set ref
230
234
  iframeRef.current = iframe;
231
- rerender();
235
+ rerender({ ver: 1 } as Props);
232
236
 
233
237
  await waitFor(() => {
234
238
  expect(result.current).toBeDefined();
@@ -302,31 +306,39 @@ describe('React Hooks', () => {
302
306
  });
303
307
 
304
308
  describe('useServer', () => {
305
- it('should create server instance', () => {
309
+ it('should create server instance', async () => {
306
310
  const { result } = renderHook(() => useServer());
307
311
 
308
- expect(result.current).toBeDefined();
309
- if (result.current) {
310
- expect(result.current.isOpen).toBe(true);
311
- }
312
+ await waitFor(() => {
313
+ expect(result.current).toBeDefined();
314
+ expect(result.current).not.toBeNull();
315
+ if (result.current) {
316
+ expect(result.current.isOpen).toBe(true);
317
+ }
318
+ }, { timeout: 2000 });
312
319
  });
313
320
 
314
- it('should create server with options', () => {
321
+ it('should create server with options', async () => {
315
322
  const options = { secretKey: 'test-key', ackTimeout: 1000 };
316
323
  const { result } = renderHook(() => useServer(options));
317
324
 
318
- expect(result.current).toBeDefined();
319
- if (result.current) {
320
- expect(result.current.secretKey).toBe('test-key');
321
- }
325
+ await waitFor(() => {
326
+ expect(result.current).toBeDefined();
327
+ expect(result.current).not.toBeNull();
328
+ if (result.current) {
329
+ expect(result.current.secretKey).toBe('test-key');
330
+ }
331
+ }, { timeout: 2000 });
322
332
  });
323
333
 
324
334
  it('should destroy server on unmount', async () => {
325
335
  const { result, unmount } = renderHook(() => useServer());
336
+ await waitFor(() => {
337
+ expect(result.current).toBeDefined();
338
+ expect(result.current).not.toBeNull();
339
+ }, { timeout: 2000 });
326
340
  const server = result.current;
327
341
 
328
- expect(server).toBeDefined();
329
-
330
342
  unmount();
331
343
 
332
344
  // Server should be destroyed
@@ -335,16 +347,20 @@ describe('React Hooks', () => {
335
347
  }
336
348
  });
337
349
 
338
- it('should create server only once on mount', () => {
350
+ it('should create server only once on mount', async () => {
339
351
  const { result, rerender } = renderHook(() => useServer());
352
+ await waitFor(() => {
353
+ expect(result.current).toBeDefined();
354
+ expect(result.current).not.toBeNull();
355
+ }, { timeout: 2000 });
340
356
  const server1 = result.current;
341
357
 
342
- expect(server1).toBeDefined();
343
-
344
358
  rerender();
345
359
 
346
360
  // Should return the same instance when deps is not provided (default empty array)
347
- expect(result.current).toBeDefined();
361
+ await waitFor(() => {
362
+ expect(result.current).toBeDefined();
363
+ }, { timeout: 2000 });
348
364
  // Note: When deps is not provided, useEffect runs only once, so server should be the same
349
365
  // But if deps changes, a new server might be created
350
366
  expect(result.current).toBe(server1);
@@ -373,7 +389,7 @@ describe('React Hooks', () => {
373
389
  });
374
390
 
375
391
  describe('useServerHandler', () => {
376
- it('should register handler when server is available', () => {
392
+ it('should register handler when server is available', async () => {
377
393
  const handler = jest.fn((req, res) => {
378
394
  res.send({ success: true });
379
395
  });
@@ -388,7 +404,10 @@ describe('React Hooks', () => {
388
404
  // Verify handler is registered by checking server internals
389
405
  // Since we can't easily test the full message flow, we just verify
390
406
  // that the hook doesn't throw and the server is created
391
- expect(serverInstance).toBeDefined();
407
+ await waitFor(() => {
408
+ expect(serverInstance).toBeDefined();
409
+ expect(serverInstance).not.toBeNull();
410
+ }, { timeout: 2000 });
392
411
  expect(handler).not.toHaveBeenCalled(); // Handler not called yet, just registered
393
412
  });
394
413
 
@@ -537,7 +556,7 @@ describe('React Hooks', () => {
537
556
  });
538
557
 
539
558
  describe('useServerHandlerMap', () => {
540
- it('should register handlers using map when server is available', () => {
559
+ it('should register handlers using map when server is available', async () => {
541
560
  const handlers = {
542
561
  'api/user': jest.fn((req, res) => res.send({ user: 'test' })),
543
562
  'api/post': jest.fn((req, res) => res.send({ post: 'test' }))
@@ -551,7 +570,10 @@ describe('React Hooks', () => {
551
570
  });
552
571
 
553
572
  // Verify server is created and handlers are registered
554
- expect(serverInstance).toBeDefined();
573
+ await waitFor(() => {
574
+ expect(serverInstance).toBeDefined();
575
+ expect(serverInstance).not.toBeNull();
576
+ }, { timeout: 2000 });
555
577
  // Handlers not called yet, just registered
556
578
  expect(handlers['api/user']).not.toHaveBeenCalled();
557
579
  expect(handlers['api/post']).not.toHaveBeenCalled();
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC1F,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,aAAa,EACnB,MAAM,OAAO,CAAC;AAEf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,SAAS,CACvB,aAAa,EAAE,CAAC,MAAM,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAAC,EACvG,OAAO,CAAC,EAAE,0BAA0B,EACpC,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,GACxB,mBAAmB,GAAG,IAAI,CAkC5B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CACvB,OAAO,CAAC,EAAE,0BAA0B,EACpC,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,GACxB,mBAAmB,GAAG,IAAI,CAkB5B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,mBAAmB,GAAG,IAAI,EAClC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,SAAS,OAAO,EAAE,GACvB,IAAI,CAmBN;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,mBAAmB,GAAG,IAAI,EAClC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EAClC,IAAI,EAAE,SAAS,OAAO,EAAE,GACvB,IAAI,CA2BN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC1F,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,aAAa,EACnB,MAAM,OAAO,CAAC;AAEf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,SAAS,CACvB,aAAa,EAAE,CAAC,MAAM,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAAC,EACvG,OAAO,CAAC,EAAE,0BAA0B,EACpC,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,GACxB,mBAAmB,GAAG,IAAI,CA2E5B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CACvB,OAAO,CAAC,EAAE,0BAA0B,EACpC,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,GACxB,mBAAmB,GAAG,IAAI,CA2C5B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,mBAAmB,GAAG,IAAI,EAClC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,SAAS,OAAO,EAAE,GACvB,IAAI,CAmBN;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,mBAAmB,GAAG,IAAI,EAClC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EAClC,IAAI,EAAE,SAAS,OAAO,EAAE,GACvB,IAAI,CA2BN"}
@@ -55,37 +55,73 @@ var _ = require("../..");
55
55
  * ```
56
56
  */
57
57
  function useClient(targetFnOrRef, options, deps) {
58
+ var clientRef = (0, _react.useRef)(null);
58
59
  var _useState = (0, _react.useState)(null),
59
60
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
60
61
  client = _useState2[0],
61
62
  setClient = _useState2[1];
62
- (0, _react.useEffect)(() => {
63
- // Get current target
64
- var target = typeof targetFnOrRef === 'function' ? targetFnOrRef() : targetFnOrRef.current;
63
+ var lastTargetRef = (0, _react.useRef)(null);
64
+ var targetFnOrRefRef = (0, _react.useRef)(targetFnOrRef);
65
+ var optionsRef = (0, _react.useRef)(options);
65
66
 
66
- // Destroy existing client if it exists
67
- if (client) {
68
- client.destroy();
69
- setClient(null);
67
+ /** Keep latest inputs without re-creating effect deps */
68
+ targetFnOrRefRef.current = targetFnOrRef;
69
+ optionsRef.current = options;
70
+ var getTarget = (0, _react.useCallback)(() => {
71
+ return typeof targetFnOrRefRef.current === 'function' ? targetFnOrRefRef.current() : targetFnOrRefRef.current.current;
72
+ }, []);
73
+
74
+ /**
75
+ * Snapshot the current target during render (pure read).
76
+ * We use this value as an effect dependency so the effect only runs when
77
+ * the target actually changes (avoids StrictMode update-depth loops).
78
+ */
79
+ var target = getTarget();
80
+ var destroy = (0, _react.useCallback)(() => {
81
+ if (clientRef.current) {
82
+ clientRef.current.destroy();
83
+ clientRef.current = null;
70
84
  }
85
+ lastTargetRef.current = null;
86
+ }, []);
87
+
88
+ /**
89
+ * Create/destroy client in effect to be compatible with React 18 StrictMode
90
+ * and concurrent rendering (avoid render-phase side effects).
91
+ */
92
+ (0, _react.useEffect)(() => {
93
+ /** If target unchanged, keep current client */
94
+ if (target === lastTargetRef.current) return;
71
95
 
72
- // Only create client if target is available
96
+ /** Target changed: destroy old client and maybe create a new one */
97
+ if (clientRef.current) {
98
+ clientRef.current.destroy();
99
+ clientRef.current = null;
100
+ }
101
+ lastTargetRef.current = target;
73
102
  if (!target) {
103
+ setClient(null);
74
104
  return;
75
105
  }
76
-
77
- // Create new client instance
78
- var newClient = (0, _.requestIframeClient)(target, options);
106
+ var newClient = (0, _.requestIframeClient)(target, optionsRef.current);
107
+ clientRef.current = newClient;
79
108
  setClient(newClient);
80
-
81
- // Cleanup: destroy client on unmount
82
109
  return () => {
83
- if (newClient) {
110
+ /** Cleanup only if it's still the current client */
111
+ if (clientRef.current === newClient) {
84
112
  newClient.destroy();
85
- setClient(null);
113
+ clientRef.current = null;
114
+ lastTargetRef.current = null;
86
115
  }
87
116
  };
88
- }, deps !== undefined ? deps : []);
117
+ }, deps ? [...deps, target] : [target]);
118
+
119
+ // Cleanup on unmount
120
+ (0, _react.useEffect)(() => {
121
+ return () => {
122
+ destroy();
123
+ };
124
+ }, []);
89
125
  return client;
90
126
  }
91
127
 
@@ -113,24 +149,46 @@ function useClient(targetFnOrRef, options, deps) {
113
149
  * ```
114
150
  */
115
151
  function useServer(options, deps) {
152
+ var serverRef = (0, _react.useRef)(null);
116
153
  var _useState3 = (0, _react.useState)(null),
117
154
  _useState4 = (0, _slicedToArray2.default)(_useState3, 2),
118
155
  server = _useState4[0],
119
156
  setServer = _useState4[1];
157
+ var optionsRef = (0, _react.useRef)(options);
158
+ optionsRef.current = options;
159
+ var destroy = (0, _react.useCallback)(() => {
160
+ if (serverRef.current) {
161
+ serverRef.current.destroy();
162
+ serverRef.current = null;
163
+ }
164
+ }, []);
165
+
166
+ /**
167
+ * Create/destroy server in effect to be compatible with React 18 StrictMode
168
+ * and concurrent rendering (avoid render-phase side effects).
169
+ */
120
170
  (0, _react.useEffect)(() => {
121
- // Create server instance
122
- var newServer = (0, _.requestIframeServer)(options);
171
+ if (serverRef.current) {
172
+ serverRef.current.destroy();
173
+ serverRef.current = null;
174
+ }
175
+ var newServer = (0, _.requestIframeServer)(optionsRef.current);
176
+ serverRef.current = newServer;
123
177
  setServer(newServer);
124
-
125
- // Cleanup: destroy server on unmount
126
178
  return () => {
127
- if (newServer) {
179
+ if (serverRef.current === newServer) {
128
180
  newServer.destroy();
129
- setServer(null);
181
+ serverRef.current = null;
130
182
  }
131
183
  };
132
- }, deps !== undefined ? deps : []); // Only create once on mount by default
184
+ }, deps !== null && deps !== void 0 ? deps : []);
133
185
 
186
+ // Cleanup on unmount
187
+ (0, _react.useEffect)(() => {
188
+ return () => {
189
+ destroy();
190
+ };
191
+ }, []);
134
192
  return server;
135
193
  }
136
194
 
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@request-iframe/react",
3
+ "version": "0.0.1",
4
+ "main": "./library/index.js",
5
+ "module": "./library/index.js",
6
+ "typings": "./library/index.d.ts"
7
+ }