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.
- package/QUICKSTART.CN.md +35 -8
- package/QUICKSTART.md +35 -8
- package/README.CN.md +177 -24
- package/README.md +237 -19
- package/library/__tests__/channel.test.ts +16 -4
- package/library/__tests__/coverage-branches.test.ts +356 -0
- package/library/__tests__/debug.test.ts +22 -0
- package/library/__tests__/dispatcher.test.ts +8 -4
- package/library/__tests__/requestIframe.test.ts +1243 -87
- package/library/__tests__/stream.test.ts +92 -16
- package/library/__tests__/utils.test.ts +41 -1
- package/library/api/client.d.ts.map +1 -1
- package/library/api/client.js +1 -0
- package/library/constants/index.d.ts +2 -0
- package/library/constants/index.d.ts.map +1 -1
- package/library/constants/index.js +3 -1
- package/library/constants/messages.d.ts +3 -0
- package/library/constants/messages.d.ts.map +1 -1
- package/library/constants/messages.js +3 -0
- package/library/core/client-server.d.ts +4 -0
- package/library/core/client-server.d.ts.map +1 -1
- package/library/core/client-server.js +45 -22
- package/library/core/client.d.ts +36 -4
- package/library/core/client.d.ts.map +1 -1
- package/library/core/client.js +508 -285
- package/library/core/request.d.ts +3 -1
- package/library/core/request.d.ts.map +1 -1
- package/library/core/request.js +2 -1
- package/library/core/response.d.ts +26 -4
- package/library/core/response.d.ts.map +1 -1
- package/library/core/response.js +192 -112
- package/library/core/server.d.ts +13 -0
- package/library/core/server.d.ts.map +1 -1
- package/library/core/server.js +221 -6
- package/library/index.d.ts +2 -1
- package/library/index.d.ts.map +1 -1
- package/library/index.js +39 -3
- package/library/message/channel.d.ts +2 -2
- package/library/message/channel.d.ts.map +1 -1
- package/library/message/channel.js +5 -1
- package/library/message/dispatcher.d.ts +2 -2
- package/library/message/dispatcher.d.ts.map +1 -1
- package/library/message/dispatcher.js +6 -5
- package/library/stream/index.d.ts +11 -1
- package/library/stream/index.d.ts.map +1 -1
- package/library/stream/index.js +21 -3
- package/library/stream/types.d.ts +2 -2
- package/library/stream/types.d.ts.map +1 -1
- package/library/stream/writable-stream.d.ts +1 -1
- package/library/stream/writable-stream.d.ts.map +1 -1
- package/library/stream/writable-stream.js +87 -47
- package/library/types/index.d.ts +29 -5
- package/library/types/index.d.ts.map +1 -1
- package/library/utils/debug.d.ts.map +1 -1
- package/library/utils/debug.js +6 -2
- package/library/utils/error.d.ts +21 -0
- package/library/utils/error.d.ts.map +1 -0
- package/library/utils/error.js +34 -0
- package/library/utils/index.d.ts +21 -0
- package/library/utils/index.d.ts.map +1 -1
- package/library/utils/index.js +141 -2
- package/library/utils/path-match.d.ts +16 -0
- package/library/utils/path-match.d.ts.map +1 -1
- package/library/utils/path-match.js +65 -0
- package/package.json +2 -1
- package/react/library/__tests__/index.test.tsx +44 -22
- package/react/library/index.d.ts.map +1 -1
- package/react/library/index.js +81 -23
- package/react/package.json +7 -0
package/library/utils/index.js
CHANGED
|
@@ -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,
|
|
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
|
+
"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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
expect(result.current
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
expect(result.current
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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"}
|
package/react/library/index.js
CHANGED
|
@@ -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.
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
var lastTargetRef = (0, _react.useRef)(null);
|
|
64
|
+
var targetFnOrRefRef = (0, _react.useRef)(targetFnOrRef);
|
|
65
|
+
var optionsRef = (0, _react.useRef)(options);
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
110
|
+
/** Cleanup only if it's still the current client */
|
|
111
|
+
if (clientRef.current === newClient) {
|
|
84
112
|
newClient.destroy();
|
|
85
|
-
|
|
113
|
+
clientRef.current = null;
|
|
114
|
+
lastTargetRef.current = null;
|
|
86
115
|
}
|
|
87
116
|
};
|
|
88
|
-
}, 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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
181
|
+
serverRef.current = null;
|
|
130
182
|
}
|
|
131
183
|
};
|
|
132
|
-
}, deps !==
|
|
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
|
|