wreq-js 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/wreq-js.js CHANGED
@@ -1,13 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WebSocket = void 0;
3
+ exports.RequestError = exports.WebSocket = exports.Session = exports.Response = exports.Headers = void 0;
4
+ exports.fetch = fetch;
5
+ exports.createSession = createSession;
6
+ exports.withSession = withSession;
4
7
  exports.request = request;
5
8
  exports.getProfiles = getProfiles;
6
9
  exports.get = get;
7
10
  exports.post = post;
8
11
  exports.websocket = websocket;
12
+ const node_crypto_1 = require("node:crypto");
13
+ const node_http_1 = require("node:http");
9
14
  const types_1 = require("./types");
15
+ Object.defineProperty(exports, "RequestError", { enumerable: true, get: function () { return types_1.RequestError; } });
10
16
  let nativeBinding;
17
+ let cachedProfiles;
11
18
  function loadNativeBinding() {
12
19
  const platform = process.platform;
13
20
  const arch = process.arch;
@@ -15,15 +22,15 @@ function loadNativeBinding() {
15
22
  // napi-rs creates files like: wreq-js.linux-x64-gnu.node
16
23
  const platformArchMap = {
17
24
  darwin: {
18
- x64: 'darwin-x64',
19
- arm64: 'darwin-arm64',
25
+ x64: "darwin-x64",
26
+ arm64: "darwin-arm64",
20
27
  },
21
28
  linux: {
22
- x64: 'linux-x64-gnu',
23
- arm64: 'linux-arm64-gnu',
29
+ x64: "linux-x64-gnu",
30
+ arm64: "linux-arm64-gnu",
24
31
  },
25
32
  win32: {
26
- x64: 'win32-x64-msvc',
33
+ x64: "win32-x64-msvc",
27
34
  },
28
35
  };
29
36
  const platformArch = platformArchMap[platform]?.[arch];
@@ -36,62 +43,568 @@ function loadNativeBinding() {
36
43
  try {
37
44
  return require(`../rust/${binaryName}`);
38
45
  }
39
- catch (e1) {
46
+ catch {
40
47
  // Fallback to wreq-js.node (for local development)
41
48
  try {
42
- return require('../rust/wreq-js.node');
49
+ return require("../rust/wreq-js.node");
43
50
  }
44
- catch (e2) {
51
+ catch {
45
52
  throw new Error(`Failed to load native module for ${platform}-${arch}. ` +
46
53
  `Tried: ../rust/${binaryName} and ../rust/wreq-js.node. ` +
47
54
  `Make sure the package is installed correctly and the native module is built for your platform.`);
48
55
  }
49
56
  }
50
57
  }
51
- try {
52
- nativeBinding = loadNativeBinding();
58
+ nativeBinding = loadNativeBinding();
59
+ const websocketFinalizer = typeof FinalizationRegistry === "function"
60
+ ? new FinalizationRegistry((connection) => {
61
+ void nativeBinding.websocketClose(connection).catch(() => undefined);
62
+ })
63
+ : undefined;
64
+ const DEFAULT_BROWSER = "chrome_142";
65
+ function generateSessionId() {
66
+ const cryptoGlobal = globalThis.crypto;
67
+ if (cryptoGlobal?.randomUUID) {
68
+ return cryptoGlobal.randomUUID();
69
+ }
70
+ return (0, node_crypto_1.randomBytes)(16).toString("hex");
71
+ }
72
+ function normalizeSessionOptions(options) {
73
+ const sessionId = options?.sessionId ?? generateSessionId();
74
+ const defaults = {
75
+ browser: options?.browser ?? DEFAULT_BROWSER,
76
+ };
77
+ if (options?.proxy !== undefined) {
78
+ defaults.proxy = options.proxy;
79
+ }
80
+ if (options?.timeout !== undefined) {
81
+ defaults.timeout = options.timeout;
82
+ }
83
+ return { sessionId, defaults };
84
+ }
85
+ function isIterable(value) {
86
+ return Boolean(value) && typeof value[Symbol.iterator] === "function";
87
+ }
88
+ function isPlainObject(value) {
89
+ if (typeof value !== "object" || value === null) {
90
+ return false;
91
+ }
92
+ const proto = Object.getPrototypeOf(value);
93
+ return proto === Object.prototype || proto === null;
94
+ }
95
+ function coerceHeaderValue(value) {
96
+ return String(value);
97
+ }
98
+ class Headers {
99
+ constructor(init) {
100
+ this.store = new Map();
101
+ if (init) {
102
+ this.applyInit(init);
103
+ }
104
+ }
105
+ applyInit(init) {
106
+ if (init instanceof Headers) {
107
+ for (const [name, value] of init) {
108
+ this.append(name, value);
109
+ }
110
+ return;
111
+ }
112
+ if (Array.isArray(init) || isIterable(init)) {
113
+ for (const tuple of init) {
114
+ if (!tuple) {
115
+ continue;
116
+ }
117
+ const [name, value] = tuple;
118
+ this.append(name, value);
119
+ }
120
+ return;
121
+ }
122
+ if (isPlainObject(init)) {
123
+ for (const [name, value] of Object.entries(init)) {
124
+ if (value === undefined || value === null) {
125
+ continue;
126
+ }
127
+ this.set(name, coerceHeaderValue(value));
128
+ }
129
+ }
130
+ }
131
+ normalizeName(name) {
132
+ if (typeof name !== "string") {
133
+ throw new TypeError("Header name must be a string");
134
+ }
135
+ const trimmed = name.trim();
136
+ if (!trimmed) {
137
+ throw new TypeError("Header name must not be empty");
138
+ }
139
+ return { key: trimmed.toLowerCase(), display: trimmed };
140
+ }
141
+ assertValue(value) {
142
+ if (value === undefined || value === null) {
143
+ throw new TypeError("Header value must not be null or undefined");
144
+ }
145
+ return coerceHeaderValue(value);
146
+ }
147
+ append(name, value) {
148
+ const normalized = this.normalizeName(name);
149
+ const existing = this.store.get(normalized.key);
150
+ const coercedValue = this.assertValue(value);
151
+ if (existing) {
152
+ existing.values.push(coercedValue);
153
+ return;
154
+ }
155
+ this.store.set(normalized.key, {
156
+ name: normalized.display,
157
+ values: [coercedValue],
158
+ });
159
+ }
160
+ set(name, value) {
161
+ const normalized = this.normalizeName(name);
162
+ const coercedValue = this.assertValue(value);
163
+ this.store.set(normalized.key, {
164
+ name: normalized.display,
165
+ values: [coercedValue],
166
+ });
167
+ }
168
+ get(name) {
169
+ const normalized = this.normalizeName(name);
170
+ const entry = this.store.get(normalized.key);
171
+ return entry ? entry.values.join(", ") : null;
172
+ }
173
+ has(name) {
174
+ const normalized = this.normalizeName(name);
175
+ return this.store.has(normalized.key);
176
+ }
177
+ delete(name) {
178
+ const normalized = this.normalizeName(name);
179
+ this.store.delete(normalized.key);
180
+ }
181
+ entries() {
182
+ return this[Symbol.iterator]();
183
+ }
184
+ *keys() {
185
+ for (const [name] of this) {
186
+ yield name;
187
+ }
188
+ }
189
+ *values() {
190
+ for (const [, value] of this) {
191
+ yield value;
192
+ }
193
+ }
194
+ forEach(callback, thisArg) {
195
+ for (const [name, value] of this) {
196
+ callback.call(thisArg, value, name, this);
197
+ }
198
+ }
199
+ [Symbol.iterator]() {
200
+ const generator = function* (store) {
201
+ for (const entry of store.values()) {
202
+ yield [entry.name, entry.values.join(", ")];
203
+ }
204
+ };
205
+ return generator(this.store);
206
+ }
207
+ toObject() {
208
+ const result = {};
209
+ for (const [name, value] of this) {
210
+ result[name] = value;
211
+ }
212
+ return result;
213
+ }
214
+ }
215
+ exports.Headers = Headers;
216
+ function cloneNativeResponse(payload) {
217
+ return {
218
+ status: payload.status,
219
+ headers: { ...payload.headers },
220
+ body: payload.body,
221
+ cookies: { ...payload.cookies },
222
+ url: payload.url,
223
+ };
224
+ }
225
+ class Response {
226
+ constructor(payload, requestUrl) {
227
+ this.type = "basic";
228
+ this.bodyUsed = false;
229
+ this.payload = cloneNativeResponse(payload);
230
+ this.requestUrl = requestUrl;
231
+ this.status = payload.status;
232
+ this.statusText = node_http_1.STATUS_CODES[payload.status] ?? "";
233
+ this.ok = this.status >= 200 && this.status < 300;
234
+ this.headers = new Headers(payload.headers);
235
+ this.url = payload.url;
236
+ this.redirected = this.url !== requestUrl;
237
+ this.cookies = { ...payload.cookies };
238
+ this.body = payload.body;
239
+ }
240
+ async json() {
241
+ const text = await this.text();
242
+ return JSON.parse(text);
243
+ }
244
+ async text() {
245
+ this.assertBodyAvailable();
246
+ this.bodyUsed = true;
247
+ return this.body;
248
+ }
249
+ clone() {
250
+ if (this.bodyUsed) {
251
+ throw new TypeError("Cannot clone a Response whose body is already used");
252
+ }
253
+ return new Response(cloneNativeResponse(this.payload), this.requestUrl);
254
+ }
255
+ assertBodyAvailable() {
256
+ if (this.bodyUsed) {
257
+ throw new TypeError("Response body is already used");
258
+ }
259
+ }
260
+ }
261
+ exports.Response = Response;
262
+ class Session {
263
+ constructor(id, defaults) {
264
+ this.disposed = false;
265
+ this.id = id;
266
+ this.defaults = defaults;
267
+ }
268
+ get closed() {
269
+ return this.disposed;
270
+ }
271
+ ensureActive() {
272
+ if (this.disposed) {
273
+ throw new types_1.RequestError("Session has been closed");
274
+ }
275
+ }
276
+ enforceBrowser(browser) {
277
+ const resolved = browser ?? this.defaults.browser;
278
+ if (resolved !== this.defaults.browser) {
279
+ throw new types_1.RequestError("Session browser cannot be changed after creation");
280
+ }
281
+ return resolved;
282
+ }
283
+ enforceProxy(proxy) {
284
+ if (proxy === undefined) {
285
+ return this.defaults.proxy;
286
+ }
287
+ if ((this.defaults.proxy ?? null) !== (proxy ?? null)) {
288
+ throw new types_1.RequestError("Session proxy cannot be changed after creation");
289
+ }
290
+ return proxy;
291
+ }
292
+ async fetch(input, init) {
293
+ this.ensureActive();
294
+ const config = {
295
+ ...(init ?? {}),
296
+ session: this,
297
+ cookieMode: "session",
298
+ };
299
+ config.browser = this.enforceBrowser(config.browser);
300
+ const proxy = this.enforceProxy(config.proxy);
301
+ if (proxy !== undefined || config.proxy !== undefined) {
302
+ if (proxy === undefined) {
303
+ delete config.proxy;
304
+ }
305
+ else {
306
+ config.proxy = proxy;
307
+ }
308
+ }
309
+ if (config.timeout === undefined && this.defaults.timeout !== undefined) {
310
+ config.timeout = this.defaults.timeout;
311
+ }
312
+ return fetch(input, config);
313
+ }
314
+ async clearCookies() {
315
+ this.ensureActive();
316
+ try {
317
+ nativeBinding.clearSession(this.id);
318
+ }
319
+ catch (error) {
320
+ throw new types_1.RequestError(String(error));
321
+ }
322
+ }
323
+ async close() {
324
+ if (this.disposed) {
325
+ return;
326
+ }
327
+ this.disposed = true;
328
+ try {
329
+ nativeBinding.dropSession(this.id);
330
+ }
331
+ catch (error) {
332
+ throw new types_1.RequestError(String(error));
333
+ }
334
+ }
335
+ }
336
+ exports.Session = Session;
337
+ function resolveSessionContext(config) {
338
+ const requestedMode = config.cookieMode ?? "ephemeral";
339
+ const sessionCandidate = config.session;
340
+ const providedSessionId = typeof config.sessionId === "string" ? config.sessionId.trim() : undefined;
341
+ if (sessionCandidate && providedSessionId) {
342
+ throw new types_1.RequestError("Provide either `session` or `sessionId`, not both.");
343
+ }
344
+ if (sessionCandidate) {
345
+ if (!(sessionCandidate instanceof Session)) {
346
+ throw new types_1.RequestError("`session` must be created via createSession()");
347
+ }
348
+ if (sessionCandidate.closed) {
349
+ throw new types_1.RequestError("Session has been closed");
350
+ }
351
+ return {
352
+ sessionId: sessionCandidate.id,
353
+ cookieMode: "session",
354
+ dropAfterRequest: false,
355
+ };
356
+ }
357
+ if (providedSessionId) {
358
+ if (!providedSessionId) {
359
+ throw new types_1.RequestError("sessionId must not be empty");
360
+ }
361
+ if (requestedMode === "ephemeral") {
362
+ throw new types_1.RequestError("cookieMode 'ephemeral' cannot be combined with sessionId");
363
+ }
364
+ return {
365
+ sessionId: providedSessionId,
366
+ cookieMode: "session",
367
+ dropAfterRequest: false,
368
+ };
369
+ }
370
+ if (requestedMode === "session") {
371
+ throw new types_1.RequestError("cookieMode 'session' requires a session or sessionId");
372
+ }
373
+ return {
374
+ sessionId: generateSessionId(),
375
+ cookieMode: "ephemeral",
376
+ dropAfterRequest: true,
377
+ };
378
+ }
379
+ function createAbortError(reason) {
380
+ const fallbackMessage = typeof reason === "string" ? reason : "The operation was aborted";
381
+ if (typeof DOMException !== "undefined" && reason instanceof DOMException) {
382
+ return reason.name === "AbortError" ? reason : new DOMException(reason.message || fallbackMessage, "AbortError");
383
+ }
384
+ if (reason instanceof Error) {
385
+ reason.name = "AbortError";
386
+ return reason;
387
+ }
388
+ if (typeof DOMException !== "undefined") {
389
+ return new DOMException(fallbackMessage, "AbortError");
390
+ }
391
+ const error = new Error(fallbackMessage);
392
+ error.name = "AbortError";
393
+ return error;
394
+ }
395
+ function isAbortError(error) {
396
+ return Boolean(error) && typeof error.name === "string" && error.name === "AbortError";
397
+ }
398
+ function setupAbort(signal) {
399
+ if (!signal) {
400
+ return null;
401
+ }
402
+ if (signal.aborted) {
403
+ throw createAbortError(signal.reason);
404
+ }
405
+ let onAbort;
406
+ const promise = new Promise((_, reject) => {
407
+ onAbort = () => {
408
+ reject(createAbortError(signal.reason));
409
+ };
410
+ signal.addEventListener("abort", onAbort, { once: true });
411
+ });
412
+ const cleanup = () => {
413
+ if (onAbort) {
414
+ signal.removeEventListener("abort", onAbort);
415
+ onAbort = undefined;
416
+ }
417
+ };
418
+ return { promise, cleanup };
419
+ }
420
+ function normalizeUrlInput(input) {
421
+ const value = typeof input === "string" ? input : input.toString();
422
+ if (!value) {
423
+ throw new types_1.RequestError("URL is required");
424
+ }
425
+ try {
426
+ return new URL(value).toString();
427
+ }
428
+ catch {
429
+ throw new types_1.RequestError(`Invalid URL: ${value}`);
430
+ }
53
431
  }
54
- catch (error) {
55
- throw error;
432
+ function validateRedirectMode(mode) {
433
+ if (!mode || mode === "follow") {
434
+ return;
435
+ }
436
+ throw new types_1.RequestError(`Redirect mode '${mode}' is not supported`);
437
+ }
438
+ function serializeBody(body) {
439
+ if (body === null || body === undefined) {
440
+ return undefined;
441
+ }
442
+ if (typeof body === "string") {
443
+ return body;
444
+ }
445
+ if (Buffer.isBuffer(body)) {
446
+ return body.toString();
447
+ }
448
+ if (body instanceof URLSearchParams) {
449
+ return body.toString();
450
+ }
451
+ if (body instanceof ArrayBuffer) {
452
+ return Buffer.from(body).toString();
453
+ }
454
+ if (ArrayBuffer.isView(body)) {
455
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString();
456
+ }
457
+ throw new TypeError("Unsupported body type; expected string, Buffer, ArrayBuffer, or URLSearchParams");
458
+ }
459
+ const SUPPORTED_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"];
460
+ function ensureMethod(method) {
461
+ const normalized = method?.trim().toUpperCase();
462
+ return normalized && normalized.length > 0 ? normalized : "GET";
463
+ }
464
+ function assertSupportedMethod(method) {
465
+ if (!SUPPORTED_METHODS.includes(method)) {
466
+ throw new types_1.RequestError(`Unsupported HTTP method: ${method}`);
467
+ }
468
+ }
469
+ function ensureBodyAllowed(method, body) {
470
+ if (!body) {
471
+ return;
472
+ }
473
+ if (method === "GET" || method === "HEAD") {
474
+ throw new types_1.RequestError(`Request with ${method} method cannot have a body`);
475
+ }
476
+ }
477
+ function validateBrowserProfile(browser) {
478
+ if (!browser) {
479
+ return;
480
+ }
481
+ const profiles = getProfiles();
482
+ if (!profiles.includes(browser)) {
483
+ throw new types_1.RequestError(`Invalid browser profile: ${browser}. Available profiles: ${profiles.join(", ")}`);
484
+ }
485
+ }
486
+ async function dispatchRequest(options, requestUrl, signal) {
487
+ const abortHandler = setupAbort(signal);
488
+ const nativePromise = nativeBinding.request(options);
489
+ const pending = abortHandler ? Promise.race([nativePromise, abortHandler.promise]) : nativePromise;
490
+ let payload;
491
+ try {
492
+ payload = (await pending);
493
+ }
494
+ catch (error) {
495
+ if (isAbortError(error)) {
496
+ throw error;
497
+ }
498
+ if (error instanceof types_1.RequestError) {
499
+ throw error;
500
+ }
501
+ throw new types_1.RequestError(String(error));
502
+ }
503
+ finally {
504
+ abortHandler?.cleanup();
505
+ }
506
+ return new Response(payload, requestUrl);
56
507
  }
57
508
  /**
58
- * Make an HTTP request with browser impersonation
59
- *
60
- * @param options - Request options
61
- * @returns Promise that resolves to the response
62
- *
63
- * @example
64
- * ```typescript
65
- * import { request } from 'wreq-js';
66
- *
67
- * const response = await request({
68
- * url: 'https://example.com/api',
69
- * browser: 'chrome_137',
70
- * headers: {
71
- * 'Custom-Header': 'value'
72
- * }
73
- * });
509
+ * Fetch-compatible entry point that adds browser impersonation controls.
74
510
  *
75
- * console.log(response.status); // 200
76
- * console.log(response.body); // Response body
77
- * ```
511
+ * @param input - Request URL (string or URL instance)
512
+ * @param init - Fetch-compatible init options
78
513
  */
79
- async function request(options) {
80
- if (!options.url) {
81
- throw new types_1.RequestError('URL is required');
514
+ async function fetch(input, init) {
515
+ const url = normalizeUrlInput(input);
516
+ const config = init ?? {};
517
+ const sessionContext = resolveSessionContext(config);
518
+ validateRedirectMode(config.redirect);
519
+ validateBrowserProfile(config.browser);
520
+ const headers = new Headers(config.headers);
521
+ const method = ensureMethod(config.method);
522
+ assertSupportedMethod(method);
523
+ const body = serializeBody(config.body ?? null);
524
+ ensureBodyAllowed(method, body);
525
+ const headerRecord = headers.toObject();
526
+ const hasHeaders = Object.keys(headerRecord).length > 0;
527
+ const requestOptions = {
528
+ url,
529
+ method,
530
+ ...(config.browser && { browser: config.browser }),
531
+ ...(hasHeaders && { headers: headerRecord }),
532
+ ...(body !== undefined && { body }),
533
+ ...(config.proxy !== undefined && { proxy: config.proxy }),
534
+ ...(config.timeout !== undefined && { timeout: config.timeout }),
535
+ sessionId: sessionContext.sessionId,
536
+ ephemeral: sessionContext.dropAfterRequest,
537
+ };
538
+ try {
539
+ return await dispatchRequest(requestOptions, url, config.signal ?? null);
82
540
  }
83
- if (options.browser) {
84
- const profiles = getProfiles();
85
- if (!profiles.includes(options.browser)) {
86
- throw new types_1.RequestError(`Invalid browser profile: ${options.browser}. Available profiles: ${profiles.join(', ')}`);
541
+ finally {
542
+ if (sessionContext.dropAfterRequest) {
543
+ try {
544
+ nativeBinding.dropSession(sessionContext.sessionId);
545
+ }
546
+ catch {
547
+ // ignore cleanup errors for ephemeral sessions
548
+ }
87
549
  }
88
550
  }
551
+ }
552
+ async function createSession(options) {
553
+ const { sessionId, defaults } = normalizeSessionOptions(options);
554
+ validateBrowserProfile(defaults.browser);
555
+ let createdId;
89
556
  try {
90
- return await nativeBinding.request(options);
557
+ createdId = nativeBinding.createSession({
558
+ sessionId,
559
+ browser: defaults.browser,
560
+ ...(defaults.proxy !== undefined && { proxy: defaults.proxy }),
561
+ });
91
562
  }
92
563
  catch (error) {
93
564
  throw new types_1.RequestError(String(error));
94
565
  }
566
+ return new Session(createdId, defaults);
567
+ }
568
+ async function withSession(fn, options) {
569
+ const session = await createSession(options);
570
+ try {
571
+ return await fn(session);
572
+ }
573
+ finally {
574
+ await session.close();
575
+ }
576
+ }
577
+ /**
578
+ * @deprecated Use {@link fetch} instead.
579
+ */
580
+ async function request(options) {
581
+ if (!options.url) {
582
+ throw new types_1.RequestError("URL is required");
583
+ }
584
+ const { url, ...rest } = options;
585
+ const init = {};
586
+ if (rest.method !== undefined) {
587
+ init.method = rest.method;
588
+ }
589
+ if (rest.headers !== undefined) {
590
+ init.headers = rest.headers;
591
+ }
592
+ if (rest.body !== undefined) {
593
+ init.body = rest.body;
594
+ }
595
+ if (rest.browser !== undefined) {
596
+ init.browser = rest.browser;
597
+ }
598
+ if (rest.proxy !== undefined) {
599
+ init.proxy = rest.proxy;
600
+ }
601
+ if (rest.timeout !== undefined) {
602
+ init.timeout = rest.timeout;
603
+ }
604
+ if (rest.sessionId !== undefined) {
605
+ init.sessionId = rest.sessionId;
606
+ }
607
+ return fetch(url, init);
95
608
  }
96
609
  /**
97
610
  * Get list of available browser profiles
@@ -107,46 +620,27 @@ async function request(options) {
107
620
  * ```
108
621
  */
109
622
  function getProfiles() {
110
- return nativeBinding.getProfiles();
623
+ if (!cachedProfiles) {
624
+ cachedProfiles = nativeBinding.getProfiles();
625
+ }
626
+ return cachedProfiles;
111
627
  }
112
628
  /**
113
- * Convenience function for GET requests
114
- *
115
- * @param url - URL to request
116
- * @param options - Additional request options
117
- * @returns Promise that resolves to the response
118
- *
119
- * @example
120
- * ```typescript
121
- * import { get } from 'wreq-js';
122
- *
123
- * const response = await get('https://example.com/api');
124
- * ```
629
+ * Convenience helper for GET requests using {@link fetch}.
125
630
  */
126
- async function get(url, options) {
127
- return request({ ...options, url, method: 'GET' });
631
+ async function get(url, init) {
632
+ return fetch(url, { ...(init ?? {}), method: "GET" });
128
633
  }
129
634
  /**
130
- * Convenience function for POST requests
131
- *
132
- * @param url - URL to request
133
- * @param body - Request body
134
- * @param options - Additional request options
135
- * @returns Promise that resolves to the response
136
- *
137
- * @example
138
- * ```typescript
139
- * import { post } from 'wreq-js';
140
- *
141
- * const response = await post(
142
- * 'https://example.com/api',
143
- * JSON.stringify({ foo: 'bar' }),
144
- * { headers: { 'Content-Type': 'application/json' } }
145
- * );
146
- * ```
635
+ * Convenience helper for POST requests using {@link fetch}.
147
636
  */
148
- async function post(url, body, options) {
149
- return request({ ...options, url, method: 'POST', body });
637
+ async function post(url, body, init) {
638
+ const config = {
639
+ ...(init ?? {}),
640
+ method: "POST",
641
+ ...(body !== undefined ? { body } : {}),
642
+ };
643
+ return fetch(url, config);
150
644
  }
151
645
  /**
152
646
  * WebSocket connection class
@@ -157,7 +651,7 @@ async function post(url, body, options) {
157
651
  *
158
652
  * const ws = await websocket({
159
653
  * url: 'wss://echo.websocket.org',
160
- * browser: 'chrome_137',
654
+ * browser: 'chrome_142',
161
655
  * onMessage: (data) => {
162
656
  * console.log('Received:', data);
163
657
  * },
@@ -181,7 +675,12 @@ async function post(url, body, options) {
181
675
  */
182
676
  class WebSocket {
183
677
  constructor(connection) {
678
+ this._closed = false;
184
679
  this._connection = connection;
680
+ if (websocketFinalizer) {
681
+ this._finalizerToken = connection;
682
+ websocketFinalizer.register(this, connection, connection);
683
+ }
185
684
  }
186
685
  /**
187
686
  * Send a message (text or binary)
@@ -198,6 +697,14 @@ class WebSocket {
198
697
  * Close the WebSocket connection
199
698
  */
200
699
  async close() {
700
+ if (this._closed) {
701
+ return;
702
+ }
703
+ this._closed = true;
704
+ if (this._finalizerToken && websocketFinalizer) {
705
+ websocketFinalizer.unregister(this._finalizerToken);
706
+ this._finalizerToken = undefined;
707
+ }
201
708
  try {
202
709
  await nativeBinding.websocketClose(this._connection);
203
710
  }
@@ -215,26 +722,26 @@ exports.WebSocket = WebSocket;
215
722
  */
216
723
  async function websocket(options) {
217
724
  if (!options.url) {
218
- throw new types_1.RequestError('URL is required');
725
+ throw new types_1.RequestError("URL is required");
219
726
  }
220
727
  if (!options.onMessage) {
221
- throw new types_1.RequestError('onMessage callback is required');
728
+ throw new types_1.RequestError("onMessage callback is required");
222
729
  }
223
730
  if (options.browser) {
224
731
  const profiles = getProfiles();
225
732
  if (!profiles.includes(options.browser)) {
226
- throw new types_1.RequestError(`Invalid browser profile: ${options.browser}. Available profiles: ${profiles.join(', ')}`);
733
+ throw new types_1.RequestError(`Invalid browser profile: ${options.browser}. Available profiles: ${profiles.join(", ")}`);
227
734
  }
228
735
  }
229
736
  try {
230
737
  const connection = await nativeBinding.websocketConnect({
231
738
  url: options.url,
232
- browser: options.browser || 'chrome_137',
739
+ browser: options.browser || DEFAULT_BROWSER,
233
740
  headers: options.headers || {},
234
- proxy: options.proxy,
741
+ ...(options.proxy !== undefined && { proxy: options.proxy }),
235
742
  onMessage: options.onMessage,
236
- onClose: options.onClose,
237
- onError: options.onError,
743
+ ...(options.onClose !== undefined && { onClose: options.onClose }),
744
+ ...(options.onError !== undefined && { onError: options.onError }),
238
745
  });
239
746
  return new WebSocket(connection);
240
747
  }
@@ -243,11 +750,17 @@ async function websocket(options) {
243
750
  }
244
751
  }
245
752
  exports.default = {
753
+ fetch,
246
754
  request,
247
755
  get,
248
756
  post,
249
757
  getProfiles,
758
+ createSession,
759
+ withSession,
250
760
  websocket,
251
761
  WebSocket,
762
+ Headers,
763
+ Response,
764
+ Session,
252
765
  };
253
766
  //# sourceMappingURL=wreq-js.js.map