@spoosh/react 0.12.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
@@ -662,32 +662,345 @@ 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
+ 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
+
665
954
  // src/create/index.ts
666
955
  function create(instance) {
667
- const { api, stateManager, eventEmitter, pluginExecutor } = instance;
956
+ const { api, stateManager, eventEmitter, pluginExecutor, transports } = instance;
668
957
  const useRead = createUseRead({
669
958
  api,
670
959
  stateManager,
671
960
  eventEmitter,
672
- pluginExecutor
961
+ pluginExecutor,
962
+ transports,
963
+ config: instance.config
673
964
  });
674
965
  const useWrite = createUseWrite({
675
966
  api,
676
967
  stateManager,
677
968
  eventEmitter,
678
- pluginExecutor
969
+ pluginExecutor,
970
+ transports,
971
+ config: instance.config
679
972
  });
680
973
  const usePages = createUsePages({
681
974
  api,
682
975
  stateManager,
683
976
  eventEmitter,
684
- pluginExecutor
977
+ pluginExecutor,
978
+ transports,
979
+ config: instance.config
685
980
  });
686
981
  const useQueue = createUseQueue({
687
982
  api,
688
983
  stateManager,
689
984
  eventEmitter,
690
- 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
691
1004
  });
692
1005
  const plugins = pluginExecutor.getPlugins();
693
1006
  const setupContext = {
@@ -718,6 +1031,8 @@ function create(instance) {
718
1031
  useWrite,
719
1032
  usePages,
720
1033
  useQueue,
1034
+ useSubscription,
1035
+ useSSE,
721
1036
  ...instanceApis
722
1037
  };
723
1038
  }