@spoosh/react 0.12.0 → 0.13.1-beta.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
@@ -662,32 +662,351 @@ function createUseQueue(options) {
662
662
  return useQueue;
663
663
  }
664
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
+ function isSSETransport(transport) {
787
+ const t = transport;
788
+ return typeof t.createSubscriptionAdapter === "function" && typeof t.utils?.resolveParser === "function" && typeof t.utils?.resolveAccumulator === "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 = transport.utils.resolveParser(
877
+ parseRef.current,
878
+ data.event
879
+ );
880
+ let parsed;
881
+ try {
882
+ parsed = parser(data.data);
883
+ } catch {
884
+ parsed = data.data;
885
+ }
886
+ if (parsed === void 0) {
887
+ return;
888
+ }
889
+ const accumulator = transport.utils.resolveAccumulator(
890
+ accumulateRef.current,
891
+ data.event
892
+ );
893
+ const parsedObj = parsed;
894
+ const messageIndex = typeof parsedObj?.index === "number" ? parsedObj.index : void 0;
895
+ if (messageIndex !== void 0) {
896
+ const lastIndex = lastMessageIndexRef.current[data.event];
897
+ if (lastIndex !== void 0 && messageIndex < lastIndex) {
898
+ setAccumulatedData({});
899
+ lastMessageIndexRef.current = {};
900
+ }
901
+ lastMessageIndexRef.current[data.event] = messageIndex;
902
+ }
903
+ setAccumulatedData((prev) => {
904
+ const previousEventData = prev[data.event];
905
+ let newEventData;
906
+ try {
907
+ newEventData = accumulator(previousEventData, parsed);
908
+ } catch {
909
+ newEventData = parsed;
910
+ }
911
+ const newAccumulated = {
912
+ ...prev,
913
+ [data.event]: newEventData
914
+ };
915
+ eventEmitter?.emit(
916
+ "spoosh:subscription:accumulate",
917
+ {
918
+ queryKey: subscription._queryKey,
919
+ eventType: data.event,
920
+ accumulatedData: newAccumulated,
921
+ timestamp: Date.now()
922
+ }
923
+ );
924
+ return newAccumulated;
925
+ });
926
+ }, [subscription.data, subscription._queryKey, eventSet]);
927
+ const reset = (0, import_react6.useCallback)(() => {
928
+ setAccumulatedData({});
929
+ }, []);
930
+ const trigger = (0, import_react6.useCallback)(
931
+ async (opts) => {
932
+ reset();
933
+ const triggerOpts = {
934
+ ...opts ?? {},
935
+ maxRetries: optionsRef.current.maxRetries,
936
+ retryDelay: optionsRef.current.retryDelay
937
+ };
938
+ currentOptionsRef.current = {
939
+ ...currentOptionsRef.current,
940
+ ...triggerOpts
941
+ };
942
+ await subscription.trigger(triggerOpts);
943
+ },
944
+ [subscription.trigger, reset]
945
+ );
946
+ return {
947
+ data: Object.keys(accumulatedData).length ? accumulatedData : void 0,
948
+ error: subscription.error,
949
+ isConnected: subscription.isConnected,
950
+ loading: subscription.loading,
951
+ meta: {},
952
+ trigger,
953
+ disconnect: subscription.disconnect,
954
+ reset
955
+ };
956
+ }
957
+ return useSSE;
958
+ }
959
+
665
960
  // src/create/index.ts
666
961
  function create(instance) {
667
- const { api, stateManager, eventEmitter, pluginExecutor } = instance;
962
+ const { api, stateManager, eventEmitter, pluginExecutor, transports } = instance;
668
963
  const useRead = createUseRead({
669
964
  api,
670
965
  stateManager,
671
966
  eventEmitter,
672
- pluginExecutor
967
+ pluginExecutor,
968
+ transports,
969
+ config: instance.config
673
970
  });
674
971
  const useWrite = createUseWrite({
675
972
  api,
676
973
  stateManager,
677
974
  eventEmitter,
678
- pluginExecutor
975
+ pluginExecutor,
976
+ transports,
977
+ config: instance.config
679
978
  });
680
979
  const usePages = createUsePages({
681
980
  api,
682
981
  stateManager,
683
982
  eventEmitter,
684
- pluginExecutor
983
+ pluginExecutor,
984
+ transports,
985
+ config: instance.config
685
986
  });
686
987
  const useQueue = createUseQueue({
687
988
  api,
688
989
  stateManager,
689
990
  eventEmitter,
690
- pluginExecutor
991
+ pluginExecutor,
992
+ transports,
993
+ config: instance.config
994
+ });
995
+ const useSubscription = createUseSubscription({
996
+ api,
997
+ stateManager,
998
+ eventEmitter,
999
+ pluginExecutor,
1000
+ transports,
1001
+ config: instance.config
1002
+ });
1003
+ const useSSE = createUseSSE({
1004
+ api,
1005
+ stateManager,
1006
+ eventEmitter,
1007
+ pluginExecutor,
1008
+ transports,
1009
+ config: instance.config
691
1010
  });
692
1011
  const plugins = pluginExecutor.getPlugins();
693
1012
  const setupContext = {
@@ -718,6 +1037,8 @@ function create(instance) {
718
1037
  useWrite,
719
1038
  usePages,
720
1039
  useQueue,
1040
+ useSubscription,
1041
+ useSSE,
721
1042
  ...instanceApis
722
1043
  };
723
1044
  }