zero-query 0.9.5 → 0.9.7

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.
@@ -446,3 +446,203 @@ describe('http — AbortController integration', () => {
446
446
  expect(fetchSpy).toHaveBeenCalled();
447
447
  });
448
448
  });
449
+
450
+
451
+ // ===========================================================================
452
+ // HEAD requests
453
+ // ===========================================================================
454
+
455
+ describe('http.head', () => {
456
+ it('sends a HEAD request', async () => {
457
+ mockFetch({});
458
+ const result = await http.head('https://api.test.com/resource');
459
+ expect(fetchSpy.mock.calls[0][1].method).toBe('HEAD');
460
+ expect(result.ok).toBe(true);
461
+ expect(result.status).toBe(200);
462
+ });
463
+
464
+ it('does not send a body', async () => {
465
+ mockFetch({});
466
+ await http.head('https://api.test.com/resource');
467
+ const opts = fetchSpy.mock.calls[0][1];
468
+ expect(opts.body).toBeUndefined();
469
+ });
470
+
471
+ it('accepts per-request options', async () => {
472
+ mockFetch({});
473
+ await http.head('https://api.test.com/resource', {
474
+ headers: { 'X-Check': 'exists' },
475
+ });
476
+ const opts = fetchSpy.mock.calls[0][1];
477
+ expect(opts.headers['X-Check']).toBe('exists');
478
+ });
479
+
480
+ it('returns response headers', async () => {
481
+ mockFetch({});
482
+ const result = await http.head('https://api.test.com/resource');
483
+ expect(result.headers).toBeDefined();
484
+ expect(typeof result.headers).toBe('object');
485
+ });
486
+ });
487
+
488
+
489
+ // ===========================================================================
490
+ // Interceptor unsubscribe
491
+ // ===========================================================================
492
+
493
+ describe('http — interceptor unsubscribe', () => {
494
+ it('onRequest returns an unsubscribe function', async () => {
495
+ http.clearInterceptors();
496
+ const spy = vi.fn();
497
+ const unsub = http.onRequest(spy);
498
+ expect(typeof unsub).toBe('function');
499
+
500
+ mockFetch({});
501
+ await http.get('https://api.test.com/a');
502
+ expect(spy).toHaveBeenCalledTimes(1);
503
+
504
+ unsub();
505
+ await http.get('https://api.test.com/b');
506
+ expect(spy).toHaveBeenCalledTimes(1); // not called again
507
+ });
508
+
509
+ it('onResponse returns an unsubscribe function', async () => {
510
+ http.clearInterceptors();
511
+ const spy = vi.fn();
512
+ const unsub = http.onResponse(spy);
513
+ expect(typeof unsub).toBe('function');
514
+
515
+ mockFetch({});
516
+ await http.get('https://api.test.com/a');
517
+ expect(spy).toHaveBeenCalledTimes(1);
518
+
519
+ unsub();
520
+ await http.get('https://api.test.com/b');
521
+ expect(spy).toHaveBeenCalledTimes(1);
522
+ });
523
+
524
+ it('double-unsubscribe is safe', async () => {
525
+ http.clearInterceptors();
526
+ const spy = vi.fn();
527
+ const unsub = http.onRequest(spy);
528
+ unsub();
529
+ unsub(); // should not throw
530
+ mockFetch({});
531
+ await http.get('https://api.test.com/data');
532
+ expect(spy).not.toHaveBeenCalled();
533
+ });
534
+ });
535
+
536
+
537
+ // ===========================================================================
538
+ // clearInterceptors
539
+ // ===========================================================================
540
+
541
+ describe('http.clearInterceptors', () => {
542
+ it('clears all interceptors when called with no args', async () => {
543
+ http.clearInterceptors();
544
+ const reqSpy = vi.fn();
545
+ const resSpy = vi.fn();
546
+ http.onRequest(reqSpy);
547
+ http.onResponse(resSpy);
548
+
549
+ http.clearInterceptors();
550
+ mockFetch({});
551
+ await http.get('https://api.test.com/data');
552
+ expect(reqSpy).not.toHaveBeenCalled();
553
+ expect(resSpy).not.toHaveBeenCalled();
554
+ });
555
+
556
+ it('clears only request interceptors with "request"', async () => {
557
+ http.clearInterceptors();
558
+ const reqSpy = vi.fn();
559
+ const resSpy = vi.fn();
560
+ http.onRequest(reqSpy);
561
+ http.onResponse(resSpy);
562
+
563
+ http.clearInterceptors('request');
564
+ mockFetch({});
565
+ await http.get('https://api.test.com/data');
566
+ expect(reqSpy).not.toHaveBeenCalled();
567
+ expect(resSpy).toHaveBeenCalledTimes(1);
568
+ });
569
+
570
+ it('clears only response interceptors with "response"', async () => {
571
+ http.clearInterceptors();
572
+ const reqSpy = vi.fn();
573
+ const resSpy = vi.fn();
574
+ http.onRequest(reqSpy);
575
+ http.onResponse(resSpy);
576
+
577
+ http.clearInterceptors('response');
578
+ mockFetch({});
579
+ await http.get('https://api.test.com/data');
580
+ expect(reqSpy).toHaveBeenCalledTimes(1);
581
+ expect(resSpy).not.toHaveBeenCalled();
582
+ });
583
+ });
584
+
585
+
586
+ // ===========================================================================
587
+ // http.all — parallel requests
588
+ // ===========================================================================
589
+
590
+ describe('http.all', () => {
591
+ it('resolves all parallel requests', async () => {
592
+ mockFetch({ ok: true });
593
+ const results = await http.all([
594
+ http.get('https://api.test.com/a'),
595
+ http.get('https://api.test.com/b'),
596
+ http.get('https://api.test.com/c'),
597
+ ]);
598
+ expect(results).toHaveLength(3);
599
+ expect(results.every(r => r.ok)).toBe(true);
600
+ });
601
+
602
+ it('rejects if any request fails', async () => {
603
+ http.clearInterceptors();
604
+ mockFetch({ error: 'fail' }, false, 500);
605
+ await expect(
606
+ http.all([
607
+ http.get('https://api.test.com/a'),
608
+ http.get('https://api.test.com/b'),
609
+ ])
610
+ ).rejects.toThrow();
611
+ });
612
+
613
+ it('handles empty array', async () => {
614
+ const results = await http.all([]);
615
+ expect(results).toEqual([]);
616
+ });
617
+ });
618
+
619
+
620
+ // ===========================================================================
621
+ // http.getConfig
622
+ // ===========================================================================
623
+
624
+ describe('http.getConfig', () => {
625
+ it('returns current config', () => {
626
+ http.configure({ baseURL: 'https://myapi.com', timeout: 5000 });
627
+ const config = http.getConfig();
628
+ expect(config.baseURL).toBe('https://myapi.com');
629
+ expect(config.timeout).toBe(5000);
630
+ expect(config.headers).toBeDefined();
631
+ });
632
+
633
+ it('returns a copy (not the internal reference)', () => {
634
+ const config = http.getConfig();
635
+ config.baseURL = 'https://mutated.com';
636
+ config.headers['X-Evil'] = 'injected';
637
+ const fresh = http.getConfig();
638
+ expect(fresh.baseURL).not.toBe('https://mutated.com');
639
+ expect(fresh.headers['X-Evil']).toBeUndefined();
640
+ });
641
+
642
+ it('reflects updates after configure', () => {
643
+ http.configure({ baseURL: '' });
644
+ expect(http.getConfig().baseURL).toBe('');
645
+ http.configure({ baseURL: 'https://updated.com' });
646
+ expect(http.getConfig().baseURL).toBe('https://updated.com');
647
+ });
648
+ });
package/types/http.d.ts CHANGED
@@ -61,15 +61,26 @@ export interface HttpClient {
61
61
  patch<T = any>(url: string, data?: any, opts?: HttpRequestOptions): Promise<HttpResponse<T>>;
62
62
  /** DELETE request. */
63
63
  delete<T = any>(url: string, data?: any, opts?: HttpRequestOptions): Promise<HttpResponse<T>>;
64
+ /** HEAD request — no body, useful for checking resource existence or headers. */
65
+ head<T = any>(url: string, opts?: HttpRequestOptions): Promise<HttpResponse<T>>;
64
66
 
65
67
  /** Update default configuration for all subsequent requests. */
66
68
  configure(options: HttpConfigureOptions): void;
67
69
 
68
- /** Add a request interceptor (called before every request). */
69
- onRequest(fn: HttpRequestInterceptor): void;
70
+ /** Read-only snapshot of the current configuration. Returns a shallow copy. */
71
+ getConfig(): { baseURL: string; headers: Record<string, string>; timeout: number };
70
72
 
71
- /** Add a response interceptor (called after every response, before error check). */
72
- onResponse(fn: HttpResponseInterceptor): void;
73
+ /** Add a request interceptor (called before every request). Returns an unsubscribe function. */
74
+ onRequest(fn: HttpRequestInterceptor): () => void;
75
+
76
+ /** Add a response interceptor (called after every response, before error check). Returns an unsubscribe function. */
77
+ onResponse(fn: HttpResponseInterceptor): () => void;
78
+
79
+ /** Clear interceptors. No args = all; `'request'` or `'response'` for one type. */
80
+ clearInterceptors(type?: 'request' | 'response'): void;
81
+
82
+ /** Run multiple request promises in parallel via `Promise.all`. */
83
+ all<T extends readonly Promise<HttpResponse<any>>[]>(requests: T): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
73
84
 
74
85
  /** Create a new `AbortController` for manual request cancellation. */
75
86
  createAbort(): AbortController;