@spoosh/react 0.11.0 → 0.13.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/index.js CHANGED
@@ -404,12 +404,12 @@ function createUseWrite(options) {
404
404
  return useWrite;
405
405
  }
406
406
 
407
- // src/useInfiniteRead/index.ts
407
+ // src/usePages/index.ts
408
408
  var import_react3 = require("react");
409
409
  var import_core3 = require("@spoosh/core");
410
- function createUseInfiniteRead(options) {
410
+ function createUsePages(options) {
411
411
  const { api, stateManager, eventEmitter, pluginExecutor } = options;
412
- return function useInfiniteRead(readFn, readOptions) {
412
+ return function usePages(readFn, readOptions) {
413
413
  const {
414
414
  enabled = true,
415
415
  tags,
@@ -432,7 +432,7 @@ function createUseInfiniteRead(options) {
432
432
  const capturedCall = selectorResultRef.current.call;
433
433
  if (!capturedCall) {
434
434
  throw new Error(
435
- 'useInfiniteRead requires calling an HTTP method (GET). Example: useInfiniteRead((api) => api("posts").GET())'
435
+ 'usePages requires calling an HTTP method (GET). Example: usePages((api) => api("posts").GET())'
436
436
  );
437
437
  }
438
438
  const requestOptions = capturedCall.options;
@@ -442,12 +442,6 @@ function createUseInfiniteRead(options) {
442
442
  params: requestOptions?.params,
443
443
  body: requestOptions?.body
444
444
  };
445
- const baseOptionsForKey = {
446
- ...capturedCall.options,
447
- query: void 0,
448
- params: void 0,
449
- body: void 0
450
- };
451
445
  const resolvedPath = (0, import_core3.resolvePath)(pathSegments, requestOptions?.params);
452
446
  const resolvedTags = (0, import_core3.resolveTags)({ tags }, resolvedPath);
453
447
  const canFetchNextRef = (0, import_react3.useRef)(canFetchNext);
@@ -463,22 +457,31 @@ function createUseInfiniteRead(options) {
463
457
  const queryKey = stateManager.createQueryKey({
464
458
  path: capturedCall.path,
465
459
  method: capturedCall.method,
466
- options: baseOptionsForKey
460
+ options: capturedCall.options
461
+ });
462
+ const lifecycleRef = (0, import_react3.useRef)({
463
+ initialized: false,
464
+ prevContext: null,
465
+ lastQueryKey: null
467
466
  });
468
467
  const controllerRef = (0, import_react3.useRef)(null);
469
- if (!controllerRef.current || controllerRef.current.queryKey !== queryKey) {
468
+ const queryKeyChanged = controllerRef.current !== null && controllerRef.current.queryKey !== queryKey;
469
+ if (queryKeyChanged) {
470
+ lifecycleRef.current.prevContext = controllerRef.current.controller.getContext();
471
+ lifecycleRef.current.initialized = false;
472
+ }
473
+ if (!controllerRef.current || queryKeyChanged) {
470
474
  controllerRef.current = {
471
475
  controller: (0, import_core3.createInfiniteReadController)({
472
476
  path: capturedCall.path,
473
477
  method: capturedCall.method,
474
478
  tags: resolvedTags,
475
479
  initialRequest,
476
- baseOptionsForKey,
477
- canFetchNext: (ctx) => canFetchNextRef.current(ctx),
480
+ canFetchNext: canFetchNext ? (ctx) => canFetchNextRef.current?.(ctx) ?? false : void 0,
478
481
  canFetchPrev: canFetchPrev ? (ctx) => canFetchPrevRef.current?.(ctx) ?? false : void 0,
479
- nextPageRequest: (ctx) => nextPageRequestRef.current(ctx),
482
+ nextPageRequest: nextPageRequest ? (ctx) => nextPageRequestRef.current?.(ctx) ?? {} : void 0,
480
483
  prevPageRequest: prevPageRequest ? (ctx) => prevPageRequestRef.current?.(ctx) ?? {} : void 0,
481
- merger: (responses) => mergerRef.current(responses),
484
+ merger: (pages) => mergerRef.current(pages),
482
485
  stateManager,
483
486
  eventEmitter,
484
487
  pluginExecutor,
@@ -488,9 +491,7 @@ function createUseInfiniteRead(options) {
488
491
  const method = pathMethods[capturedCall.method];
489
492
  const fetchOptions = {
490
493
  ...capturedCall.options,
491
- query: opts.query,
492
- params: opts.params,
493
- body: opts.body,
494
+ ...opts,
494
495
  signal
495
496
  };
496
497
  return method(fetchOptions);
@@ -501,11 +502,12 @@ function createUseInfiniteRead(options) {
501
502
  }
502
503
  const controller = controllerRef.current.controller;
503
504
  controller.setPluginOptions(pluginOpts);
504
- const state = (0, import_react3.useSyncExternalStore)(
505
- controller.subscribe,
506
- controller.getState,
507
- controller.getState
505
+ const subscribe = (0, import_react3.useCallback)(
506
+ (callback) => controller.subscribe(callback),
507
+ [controller]
508
508
  );
509
+ const getSnapshot = (0, import_react3.useCallback)(() => controller.getState(), [controller]);
510
+ const state = (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
509
511
  const [isPending, setIsPending] = (0, import_react3.useState)(() => {
510
512
  return enabled && state.data === void 0;
511
513
  });
@@ -515,10 +517,6 @@ function createUseInfiniteRead(options) {
515
517
  const fetchingPrev = fetchingDirection === "prev";
516
518
  const hasData = state.data !== void 0;
517
519
  const loading = (isPending || fetching) && !hasData;
518
- const lifecycleRef = (0, import_react3.useRef)({
519
- initialized: false,
520
- prevContext: null
521
- });
522
520
  const tagsKey = JSON.stringify(tags);
523
521
  (0, import_react3.useEffect)(() => {
524
522
  return () => {
@@ -527,8 +525,27 @@ function createUseInfiniteRead(options) {
527
525
  };
528
526
  }, []);
529
527
  (0, import_react3.useEffect)(() => {
530
- controller.mount();
531
- lifecycleRef.current.initialized = true;
528
+ if (!enabled) return;
529
+ const { initialized, prevContext, lastQueryKey } = lifecycleRef.current;
530
+ const isQueryKeyChange = lastQueryKey !== null && lastQueryKey !== queryKey;
531
+ if (!initialized) {
532
+ controller.mount();
533
+ lifecycleRef.current.initialized = true;
534
+ if (prevContext) {
535
+ controller.update(prevContext);
536
+ lifecycleRef.current.prevContext = null;
537
+ }
538
+ }
539
+ lifecycleRef.current.lastQueryKey = queryKey;
540
+ const currentState = controller.getState();
541
+ const isFetching = controller.getFetchingDirection() !== null;
542
+ if (isQueryKeyChange) {
543
+ setIsPending(true);
544
+ controller.trigger({ force: false }).finally(() => setIsPending(false));
545
+ } else if (currentState.data === void 0 && !isFetching) {
546
+ setIsPending(true);
547
+ controller.fetchNext().finally(() => setIsPending(false));
548
+ }
532
549
  const unsubInvalidate = eventEmitter.on(
533
550
  "invalidate",
534
551
  (invalidatedTags) => {
@@ -537,41 +554,32 @@ function createUseInfiniteRead(options) {
537
554
  );
538
555
  if (hasMatch) {
539
556
  setIsPending(true);
540
- controller.refetch().finally(() => setIsPending(false));
557
+ controller.trigger().finally(() => setIsPending(false));
541
558
  }
542
559
  }
543
560
  );
544
561
  const unsubRefetchAll = eventEmitter.on("refetchAll", () => {
545
562
  setIsPending(true);
546
- controller.refetch().finally(() => setIsPending(false));
563
+ controller.trigger().finally(() => setIsPending(false));
547
564
  });
548
565
  return () => {
566
+ controller.unmount();
549
567
  unsubInvalidate();
550
568
  unsubRefetchAll();
551
569
  };
552
- }, [tagsKey]);
553
- (0, import_react3.useEffect)(() => {
554
- if (!lifecycleRef.current.initialized) return;
555
- if (enabled) {
556
- const currentState = controller.getState();
557
- const isFetching = controller.getFetchingDirection() !== null;
558
- if (currentState.data === void 0 && !isFetching) {
559
- setIsPending(true);
560
- controller.fetchNext().finally(() => setIsPending(false));
561
- }
562
- }
563
- }, [enabled]);
570
+ }, [queryKey, enabled, tagsKey]);
571
+ const pluginOptsKey = JSON.stringify(pluginOpts);
564
572
  (0, import_react3.useEffect)(() => {
565
573
  if (!enabled || !lifecycleRef.current.initialized) return;
566
574
  const prevContext = controller.getContext();
567
575
  controller.update(prevContext);
568
- }, [JSON.stringify(pluginOpts)]);
569
- const entry = stateManager.getCache(queryKey);
570
- const pluginResultData = entry?.meta ? Object.fromEntries(entry.meta) : {};
576
+ }, [pluginOptsKey]);
577
+ const trigger = async (options2) => {
578
+ await controller.trigger(options2);
579
+ };
571
580
  const result = {
572
- meta: pluginResultData,
573
581
  data: state.data,
574
- allResponses: state.allResponses,
582
+ pages: state.pages,
575
583
  loading,
576
584
  fetching,
577
585
  fetchingNext,
@@ -580,7 +588,7 @@ function createUseInfiniteRead(options) {
580
588
  canFetchPrev: state.canFetchPrev,
581
589
  fetchNext: controller.fetchNext,
582
590
  fetchPrev: controller.fetchPrev,
583
- trigger: controller.refetch,
591
+ trigger,
584
592
  abort: controller.abort,
585
593
  error: state.error
586
594
  };
@@ -654,32 +662,345 @@ function createUseQueue(options) {
654
662
  return useQueue;
655
663
  }
656
664
 
665
+ // src/useSubscription/index.ts
666
+ var import_react5 = require("react");
667
+ var import_core5 = require("@spoosh/core");
668
+ function createUseSubscription(options) {
669
+ const { stateManager, eventEmitter, pluginExecutor } = options;
670
+ function useSubscription(subFn, subOptions) {
671
+ const { enabled = true, adapter, operationType } = subOptions;
672
+ const selectorResultRef = (0, import_react5.useRef)({
673
+ call: null,
674
+ selector: null
675
+ });
676
+ const selectorProxy = (0, import_core5.createSelectorProxy)((result) => {
677
+ selectorResultRef.current = result;
678
+ });
679
+ subFn(selectorProxy);
680
+ const capturedCall = selectorResultRef.current.call;
681
+ if (!capturedCall) {
682
+ throw new Error("useSubscription requires calling a method");
683
+ }
684
+ const queryKey = stateManager.createQueryKey({
685
+ path: capturedCall.path,
686
+ method: capturedCall.method,
687
+ options: capturedCall.options
688
+ });
689
+ const controllerRef = (0, import_react5.useRef)(null);
690
+ const subscriptionVersionRef = (0, import_react5.useRef)(0);
691
+ const getOrCreateController = (0, import_react5.useCallback)(() => {
692
+ if (controllerRef.current) {
693
+ return controllerRef.current;
694
+ }
695
+ const controller = (0, import_core5.createSubscriptionController)({
696
+ channel: capturedCall.path,
697
+ baseAdapter: adapter,
698
+ stateManager,
699
+ eventEmitter,
700
+ pluginExecutor,
701
+ queryKey,
702
+ operationType,
703
+ path: capturedCall.path,
704
+ method: capturedCall.method
705
+ });
706
+ controllerRef.current = controller;
707
+ return controller;
708
+ }, [
709
+ queryKey,
710
+ adapter,
711
+ operationType,
712
+ capturedCall.path,
713
+ capturedCall.method
714
+ ]);
715
+ const subscribe = (0, import_react5.useCallback)(
716
+ (callback) => {
717
+ const controller = getOrCreateController();
718
+ return controller.subscribe(callback);
719
+ },
720
+ [getOrCreateController]
721
+ );
722
+ const emptyStateRef = (0, import_react5.useRef)({
723
+ data: void 0,
724
+ error: void 0,
725
+ isConnected: false
726
+ });
727
+ const getSnapshot = (0, import_react5.useCallback)(() => {
728
+ if (!controllerRef.current) {
729
+ return emptyStateRef.current;
730
+ }
731
+ return controllerRef.current.getState();
732
+ }, []);
733
+ const state = (0, import_react5.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
734
+ const [isPending, setIsPending] = (0, import_react5.useState)(enabled);
735
+ (0, import_react5.useEffect)(() => {
736
+ if (!enabled) {
737
+ return;
738
+ }
739
+ setIsPending(true);
740
+ const controller = getOrCreateController();
741
+ controller.mount();
742
+ controller.subscribe();
743
+ return () => {
744
+ subscriptionVersionRef.current++;
745
+ controller.unsubscribe();
746
+ };
747
+ }, [queryKey, enabled, getOrCreateController]);
748
+ (0, import_react5.useEffect)(() => {
749
+ if (state.isConnected || state.data !== void 0 || state.error !== void 0) {
750
+ setIsPending(false);
751
+ }
752
+ }, [state.isConnected, state.data, state.error]);
753
+ const disconnect = (0, import_react5.useCallback)(() => {
754
+ subscriptionVersionRef.current++;
755
+ if (controllerRef.current) {
756
+ controllerRef.current.unsubscribe();
757
+ }
758
+ }, []);
759
+ const trigger = (0, import_react5.useCallback)(async () => {
760
+ setIsPending(true);
761
+ subscriptionVersionRef.current++;
762
+ const controller = getOrCreateController();
763
+ controller.unsubscribe();
764
+ controller.mount();
765
+ await controller.subscribe();
766
+ }, [getOrCreateController]);
767
+ const loading = isPending;
768
+ return {
769
+ meta: {},
770
+ data: state.data,
771
+ error: state.error,
772
+ loading,
773
+ isConnected: state.isConnected,
774
+ _queryKey: queryKey,
775
+ _subscriptionVersion: subscriptionVersionRef.current,
776
+ trigger,
777
+ disconnect
778
+ };
779
+ }
780
+ return useSubscription;
781
+ }
782
+
783
+ // src/useSSE/index.ts
784
+ var import_react6 = require("react");
785
+ var import_core6 = require("@spoosh/core");
786
+ var import_transport_sse = require("@spoosh/transport-sse");
787
+ function isSSETransport(transport) {
788
+ return "createSubscriptionAdapter" in transport && typeof transport.createSubscriptionAdapter === "function";
789
+ }
790
+ function createUseSSE(options) {
791
+ const { eventEmitter, transports, config } = options;
792
+ const useSubscription = createUseSubscription(options);
793
+ function useSSE(subFn, sseOptions) {
794
+ const {
795
+ enabled = true,
796
+ events,
797
+ parse = "auto",
798
+ accumulate = "replace",
799
+ maxRetries,
800
+ retryDelay
801
+ } = sseOptions ?? {};
802
+ const transport = transports.get("sse");
803
+ if (!transport) {
804
+ throw new Error(
805
+ "SSE transport not registered. Make sure to register an SSE transport before using useSSE."
806
+ );
807
+ }
808
+ if (!isSSETransport(transport)) {
809
+ throw new Error(
810
+ "SSE transport does not implement createSubscriptionAdapter."
811
+ );
812
+ }
813
+ const selectorResultRef = (0, import_react6.useRef)({
814
+ call: null,
815
+ selector: null
816
+ });
817
+ const selectorProxy = (0, import_core6.createSelectorProxy)((result) => {
818
+ selectorResultRef.current = result;
819
+ });
820
+ subFn(selectorProxy);
821
+ const capturedCall = selectorResultRef.current.call;
822
+ if (!capturedCall) {
823
+ throw new Error("useSSE requires calling a method");
824
+ }
825
+ const currentOptionsRef = (0, import_react6.useRef)(
826
+ capturedCall.options
827
+ );
828
+ const adapter = (0, import_react6.useMemo)(
829
+ () => transport.createSubscriptionAdapter({
830
+ channel: capturedCall.path,
831
+ method: capturedCall.method,
832
+ baseUrl: config.baseUrl,
833
+ globalHeaders: config.defaultOptions.headers,
834
+ getRequestOptions: () => currentOptionsRef.current,
835
+ eventEmitter,
836
+ devtoolMeta: events ? { listenedEvents: events } : void 0
837
+ }),
838
+ [capturedCall.path, capturedCall.method]
839
+ );
840
+ const [accumulatedData, setAccumulatedData] = (0, import_react6.useState)({});
841
+ const eventSet = (0, import_react6.useMemo)(
842
+ () => events ? new Set(events) : null,
843
+ [events?.join(",")]
844
+ );
845
+ const parseRef = (0, import_react6.useRef)(parse);
846
+ const accumulateRef = (0, import_react6.useRef)(accumulate);
847
+ parseRef.current = parse;
848
+ accumulateRef.current = accumulate;
849
+ const optionsRef = (0, import_react6.useRef)({
850
+ maxRetries,
851
+ retryDelay
852
+ });
853
+ optionsRef.current = { maxRetries, retryDelay };
854
+ const subscription = useSubscription(subFn, {
855
+ enabled,
856
+ adapter,
857
+ operationType: transport.operationType
858
+ });
859
+ const prevVersionRef = (0, import_react6.useRef)(subscription._subscriptionVersion);
860
+ const lastMessageIndexRef = (0, import_react6.useRef)({});
861
+ (0, import_react6.useEffect)(() => {
862
+ if (subscription._subscriptionVersion !== prevVersionRef.current) {
863
+ setAccumulatedData({});
864
+ lastMessageIndexRef.current = {};
865
+ }
866
+ prevVersionRef.current = subscription._subscriptionVersion;
867
+ }, [subscription._subscriptionVersion]);
868
+ (0, import_react6.useEffect)(() => {
869
+ const data = subscription.data;
870
+ if (!data) {
871
+ return;
872
+ }
873
+ if (eventSet && !eventSet.has(data.event)) {
874
+ return;
875
+ }
876
+ const parser = (0, import_transport_sse.resolveParser)(parseRef.current, data.event);
877
+ let parsed;
878
+ try {
879
+ parsed = parser(data.data);
880
+ } catch {
881
+ parsed = data.data;
882
+ }
883
+ if (parsed === void 0) {
884
+ return;
885
+ }
886
+ const accumulator = (0, import_transport_sse.resolveAccumulator)(accumulateRef.current, data.event);
887
+ const parsedObj = parsed;
888
+ const messageIndex = typeof parsedObj?.index === "number" ? parsedObj.index : void 0;
889
+ if (messageIndex !== void 0) {
890
+ const lastIndex = lastMessageIndexRef.current[data.event];
891
+ if (lastIndex !== void 0 && messageIndex < lastIndex) {
892
+ setAccumulatedData({});
893
+ lastMessageIndexRef.current = {};
894
+ }
895
+ lastMessageIndexRef.current[data.event] = messageIndex;
896
+ }
897
+ setAccumulatedData((prev) => {
898
+ const previousEventData = prev[data.event];
899
+ let newEventData;
900
+ try {
901
+ newEventData = accumulator(previousEventData, parsed);
902
+ } catch {
903
+ newEventData = parsed;
904
+ }
905
+ const newAccumulated = {
906
+ ...prev,
907
+ [data.event]: newEventData
908
+ };
909
+ eventEmitter?.emit(
910
+ "spoosh:subscription:accumulate",
911
+ {
912
+ queryKey: subscription._queryKey,
913
+ eventType: data.event,
914
+ accumulatedData: newAccumulated,
915
+ timestamp: Date.now()
916
+ }
917
+ );
918
+ return newAccumulated;
919
+ });
920
+ }, [subscription.data, subscription._queryKey, eventSet]);
921
+ const reset = (0, import_react6.useCallback)(() => {
922
+ setAccumulatedData({});
923
+ }, []);
924
+ const trigger = (0, import_react6.useCallback)(
925
+ async (opts) => {
926
+ reset();
927
+ const triggerOpts = {
928
+ ...opts ?? {},
929
+ maxRetries: optionsRef.current.maxRetries,
930
+ retryDelay: optionsRef.current.retryDelay
931
+ };
932
+ currentOptionsRef.current = {
933
+ ...currentOptionsRef.current,
934
+ ...triggerOpts
935
+ };
936
+ await subscription.trigger(triggerOpts);
937
+ },
938
+ [subscription.trigger, reset]
939
+ );
940
+ return {
941
+ data: Object.keys(accumulatedData).length ? accumulatedData : void 0,
942
+ error: subscription.error,
943
+ isConnected: subscription.isConnected,
944
+ loading: subscription.loading,
945
+ meta: {},
946
+ trigger,
947
+ disconnect: subscription.disconnect,
948
+ reset
949
+ };
950
+ }
951
+ return useSSE;
952
+ }
953
+
657
954
  // src/create/index.ts
658
955
  function create(instance) {
659
- const { api, stateManager, eventEmitter, pluginExecutor } = instance;
956
+ const { api, stateManager, eventEmitter, pluginExecutor, transports } = instance;
660
957
  const useRead = createUseRead({
661
958
  api,
662
959
  stateManager,
663
960
  eventEmitter,
664
- pluginExecutor
961
+ pluginExecutor,
962
+ transports,
963
+ config: instance.config
665
964
  });
666
965
  const useWrite = createUseWrite({
667
966
  api,
668
967
  stateManager,
669
968
  eventEmitter,
670
- pluginExecutor
969
+ pluginExecutor,
970
+ transports,
971
+ config: instance.config
671
972
  });
672
- const useInfiniteRead = createUseInfiniteRead({
973
+ const usePages = createUsePages({
673
974
  api,
674
975
  stateManager,
675
976
  eventEmitter,
676
- pluginExecutor
977
+ pluginExecutor,
978
+ transports,
979
+ config: instance.config
677
980
  });
678
981
  const useQueue = createUseQueue({
679
982
  api,
680
983
  stateManager,
681
984
  eventEmitter,
682
- pluginExecutor
985
+ pluginExecutor,
986
+ transports,
987
+ config: instance.config
988
+ });
989
+ const useSubscription = createUseSubscription({
990
+ api,
991
+ stateManager,
992
+ eventEmitter,
993
+ pluginExecutor,
994
+ transports,
995
+ config: instance.config
996
+ });
997
+ const useSSE = createUseSSE({
998
+ api,
999
+ stateManager,
1000
+ eventEmitter,
1001
+ pluginExecutor,
1002
+ transports,
1003
+ config: instance.config
683
1004
  });
684
1005
  const plugins = pluginExecutor.getPlugins();
685
1006
  const setupContext = {
@@ -708,8 +1029,10 @@ function create(instance) {
708
1029
  return {
709
1030
  useRead,
710
1031
  useWrite,
711
- useInfiniteRead,
1032
+ usePages,
712
1033
  useQueue,
1034
+ useSubscription,
1035
+ useSSE,
713
1036
  ...instanceApis
714
1037
  };
715
1038
  }