@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.mjs CHANGED
@@ -675,32 +675,360 @@ function createUseQueue(options) {
675
675
  return useQueue;
676
676
  }
677
677
 
678
+ // src/useSubscription/index.ts
679
+ import {
680
+ useSyncExternalStore as useSyncExternalStore5,
681
+ useRef as useRef5,
682
+ useEffect as useEffect4,
683
+ useCallback as useCallback4,
684
+ useState as useState4
685
+ } from "react";
686
+ import {
687
+ createSelectorProxy as createSelectorProxy5,
688
+ createSubscriptionController
689
+ } from "@spoosh/core";
690
+ function createUseSubscription(options) {
691
+ const { stateManager, eventEmitter, pluginExecutor } = options;
692
+ function useSubscription(subFn, subOptions) {
693
+ const { enabled = true, adapter, operationType } = subOptions;
694
+ const selectorResultRef = useRef5({
695
+ call: null,
696
+ selector: null
697
+ });
698
+ const selectorProxy = createSelectorProxy5((result) => {
699
+ selectorResultRef.current = result;
700
+ });
701
+ subFn(selectorProxy);
702
+ const capturedCall = selectorResultRef.current.call;
703
+ if (!capturedCall) {
704
+ throw new Error("useSubscription requires calling a method");
705
+ }
706
+ const queryKey = stateManager.createQueryKey({
707
+ path: capturedCall.path,
708
+ method: capturedCall.method,
709
+ options: capturedCall.options
710
+ });
711
+ const controllerRef = useRef5(null);
712
+ const subscriptionVersionRef = useRef5(0);
713
+ const getOrCreateController = useCallback4(() => {
714
+ if (controllerRef.current) {
715
+ return controllerRef.current;
716
+ }
717
+ const controller = createSubscriptionController({
718
+ channel: capturedCall.path,
719
+ baseAdapter: adapter,
720
+ stateManager,
721
+ eventEmitter,
722
+ pluginExecutor,
723
+ queryKey,
724
+ operationType,
725
+ path: capturedCall.path,
726
+ method: capturedCall.method
727
+ });
728
+ controllerRef.current = controller;
729
+ return controller;
730
+ }, [
731
+ queryKey,
732
+ adapter,
733
+ operationType,
734
+ capturedCall.path,
735
+ capturedCall.method
736
+ ]);
737
+ const subscribe = useCallback4(
738
+ (callback) => {
739
+ const controller = getOrCreateController();
740
+ return controller.subscribe(callback);
741
+ },
742
+ [getOrCreateController]
743
+ );
744
+ const emptyStateRef = useRef5({
745
+ data: void 0,
746
+ error: void 0,
747
+ isConnected: false
748
+ });
749
+ const getSnapshot = useCallback4(() => {
750
+ if (!controllerRef.current) {
751
+ return emptyStateRef.current;
752
+ }
753
+ return controllerRef.current.getState();
754
+ }, []);
755
+ const state = useSyncExternalStore5(subscribe, getSnapshot, getSnapshot);
756
+ const [isPending, setIsPending] = useState4(enabled);
757
+ useEffect4(() => {
758
+ if (!enabled) {
759
+ return;
760
+ }
761
+ setIsPending(true);
762
+ const controller = getOrCreateController();
763
+ controller.mount();
764
+ controller.subscribe();
765
+ return () => {
766
+ subscriptionVersionRef.current++;
767
+ controller.unsubscribe();
768
+ };
769
+ }, [queryKey, enabled, getOrCreateController]);
770
+ useEffect4(() => {
771
+ if (state.isConnected || state.data !== void 0 || state.error !== void 0) {
772
+ setIsPending(false);
773
+ }
774
+ }, [state.isConnected, state.data, state.error]);
775
+ const disconnect = useCallback4(() => {
776
+ subscriptionVersionRef.current++;
777
+ if (controllerRef.current) {
778
+ controllerRef.current.unsubscribe();
779
+ }
780
+ }, []);
781
+ const trigger = useCallback4(async () => {
782
+ setIsPending(true);
783
+ subscriptionVersionRef.current++;
784
+ const controller = getOrCreateController();
785
+ controller.unsubscribe();
786
+ controller.mount();
787
+ await controller.subscribe();
788
+ }, [getOrCreateController]);
789
+ const loading = isPending;
790
+ return {
791
+ meta: {},
792
+ data: state.data,
793
+ error: state.error,
794
+ loading,
795
+ isConnected: state.isConnected,
796
+ _queryKey: queryKey,
797
+ _subscriptionVersion: subscriptionVersionRef.current,
798
+ trigger,
799
+ disconnect
800
+ };
801
+ }
802
+ return useSubscription;
803
+ }
804
+
805
+ // src/useSSE/index.ts
806
+ import { useState as useState5, useRef as useRef6, useEffect as useEffect5, useCallback as useCallback5, useMemo } from "react";
807
+ import { createSelectorProxy as createSelectorProxy6 } from "@spoosh/core";
808
+ function isSSETransport(transport) {
809
+ const t = transport;
810
+ return typeof t.createSubscriptionAdapter === "function" && typeof t.utils?.resolveParser === "function" && typeof t.utils?.resolveAccumulator === "function";
811
+ }
812
+ function createUseSSE(options) {
813
+ const { eventEmitter, transports, config } = options;
814
+ const useSubscription = createUseSubscription(options);
815
+ function useSSE(subFn, sseOptions) {
816
+ const {
817
+ enabled = true,
818
+ events,
819
+ parse = "auto",
820
+ accumulate = "replace",
821
+ maxRetries,
822
+ retryDelay
823
+ } = sseOptions ?? {};
824
+ const transport = transports.get("sse");
825
+ if (!transport) {
826
+ throw new Error(
827
+ "SSE transport not registered. Make sure to register an SSE transport before using useSSE."
828
+ );
829
+ }
830
+ if (!isSSETransport(transport)) {
831
+ throw new Error(
832
+ "SSE transport does not implement createSubscriptionAdapter."
833
+ );
834
+ }
835
+ const selectorResultRef = useRef6({
836
+ call: null,
837
+ selector: null
838
+ });
839
+ const selectorProxy = createSelectorProxy6((result) => {
840
+ selectorResultRef.current = result;
841
+ });
842
+ subFn(selectorProxy);
843
+ const capturedCall = selectorResultRef.current.call;
844
+ if (!capturedCall) {
845
+ throw new Error("useSSE requires calling a method");
846
+ }
847
+ const currentOptionsRef = useRef6(
848
+ capturedCall.options
849
+ );
850
+ const adapter = useMemo(
851
+ () => transport.createSubscriptionAdapter({
852
+ channel: capturedCall.path,
853
+ method: capturedCall.method,
854
+ baseUrl: config.baseUrl,
855
+ globalHeaders: config.defaultOptions.headers,
856
+ getRequestOptions: () => currentOptionsRef.current,
857
+ eventEmitter,
858
+ devtoolMeta: events ? { listenedEvents: events } : void 0
859
+ }),
860
+ [capturedCall.path, capturedCall.method]
861
+ );
862
+ const [accumulatedData, setAccumulatedData] = useState5({});
863
+ const eventSet = useMemo(
864
+ () => events ? new Set(events) : null,
865
+ [events?.join(",")]
866
+ );
867
+ const parseRef = useRef6(parse);
868
+ const accumulateRef = useRef6(accumulate);
869
+ parseRef.current = parse;
870
+ accumulateRef.current = accumulate;
871
+ const optionsRef = useRef6({
872
+ maxRetries,
873
+ retryDelay
874
+ });
875
+ optionsRef.current = { maxRetries, retryDelay };
876
+ const subscription = useSubscription(subFn, {
877
+ enabled,
878
+ adapter,
879
+ operationType: transport.operationType
880
+ });
881
+ const prevVersionRef = useRef6(subscription._subscriptionVersion);
882
+ const lastMessageIndexRef = useRef6({});
883
+ useEffect5(() => {
884
+ if (subscription._subscriptionVersion !== prevVersionRef.current) {
885
+ setAccumulatedData({});
886
+ lastMessageIndexRef.current = {};
887
+ }
888
+ prevVersionRef.current = subscription._subscriptionVersion;
889
+ }, [subscription._subscriptionVersion]);
890
+ useEffect5(() => {
891
+ const data = subscription.data;
892
+ if (!data) {
893
+ return;
894
+ }
895
+ if (eventSet && !eventSet.has(data.event)) {
896
+ return;
897
+ }
898
+ const parser = transport.utils.resolveParser(
899
+ parseRef.current,
900
+ data.event
901
+ );
902
+ let parsed;
903
+ try {
904
+ parsed = parser(data.data);
905
+ } catch {
906
+ parsed = data.data;
907
+ }
908
+ if (parsed === void 0) {
909
+ return;
910
+ }
911
+ const accumulator = transport.utils.resolveAccumulator(
912
+ accumulateRef.current,
913
+ data.event
914
+ );
915
+ const parsedObj = parsed;
916
+ const messageIndex = typeof parsedObj?.index === "number" ? parsedObj.index : void 0;
917
+ if (messageIndex !== void 0) {
918
+ const lastIndex = lastMessageIndexRef.current[data.event];
919
+ if (lastIndex !== void 0 && messageIndex < lastIndex) {
920
+ setAccumulatedData({});
921
+ lastMessageIndexRef.current = {};
922
+ }
923
+ lastMessageIndexRef.current[data.event] = messageIndex;
924
+ }
925
+ setAccumulatedData((prev) => {
926
+ const previousEventData = prev[data.event];
927
+ let newEventData;
928
+ try {
929
+ newEventData = accumulator(previousEventData, parsed);
930
+ } catch {
931
+ newEventData = parsed;
932
+ }
933
+ const newAccumulated = {
934
+ ...prev,
935
+ [data.event]: newEventData
936
+ };
937
+ eventEmitter?.emit(
938
+ "spoosh:subscription:accumulate",
939
+ {
940
+ queryKey: subscription._queryKey,
941
+ eventType: data.event,
942
+ accumulatedData: newAccumulated,
943
+ timestamp: Date.now()
944
+ }
945
+ );
946
+ return newAccumulated;
947
+ });
948
+ }, [subscription.data, subscription._queryKey, eventSet]);
949
+ const reset = useCallback5(() => {
950
+ setAccumulatedData({});
951
+ }, []);
952
+ const trigger = useCallback5(
953
+ async (opts) => {
954
+ reset();
955
+ const triggerOpts = {
956
+ ...opts ?? {},
957
+ maxRetries: optionsRef.current.maxRetries,
958
+ retryDelay: optionsRef.current.retryDelay
959
+ };
960
+ currentOptionsRef.current = {
961
+ ...currentOptionsRef.current,
962
+ ...triggerOpts
963
+ };
964
+ await subscription.trigger(triggerOpts);
965
+ },
966
+ [subscription.trigger, reset]
967
+ );
968
+ return {
969
+ data: Object.keys(accumulatedData).length ? accumulatedData : void 0,
970
+ error: subscription.error,
971
+ isConnected: subscription.isConnected,
972
+ loading: subscription.loading,
973
+ meta: {},
974
+ trigger,
975
+ disconnect: subscription.disconnect,
976
+ reset
977
+ };
978
+ }
979
+ return useSSE;
980
+ }
981
+
678
982
  // src/create/index.ts
679
983
  function create(instance) {
680
- const { api, stateManager, eventEmitter, pluginExecutor } = instance;
984
+ const { api, stateManager, eventEmitter, pluginExecutor, transports } = instance;
681
985
  const useRead = createUseRead({
682
986
  api,
683
987
  stateManager,
684
988
  eventEmitter,
685
- pluginExecutor
989
+ pluginExecutor,
990
+ transports,
991
+ config: instance.config
686
992
  });
687
993
  const useWrite = createUseWrite({
688
994
  api,
689
995
  stateManager,
690
996
  eventEmitter,
691
- pluginExecutor
997
+ pluginExecutor,
998
+ transports,
999
+ config: instance.config
692
1000
  });
693
1001
  const usePages = createUsePages({
694
1002
  api,
695
1003
  stateManager,
696
1004
  eventEmitter,
697
- pluginExecutor
1005
+ pluginExecutor,
1006
+ transports,
1007
+ config: instance.config
698
1008
  });
699
1009
  const useQueue = createUseQueue({
700
1010
  api,
701
1011
  stateManager,
702
1012
  eventEmitter,
703
- pluginExecutor
1013
+ pluginExecutor,
1014
+ transports,
1015
+ config: instance.config
1016
+ });
1017
+ const useSubscription = createUseSubscription({
1018
+ api,
1019
+ stateManager,
1020
+ eventEmitter,
1021
+ pluginExecutor,
1022
+ transports,
1023
+ config: instance.config
1024
+ });
1025
+ const useSSE = createUseSSE({
1026
+ api,
1027
+ stateManager,
1028
+ eventEmitter,
1029
+ pluginExecutor,
1030
+ transports,
1031
+ config: instance.config
704
1032
  });
705
1033
  const plugins = pluginExecutor.getPlugins();
706
1034
  const setupContext = {
@@ -731,6 +1059,8 @@ function create(instance) {
731
1059
  useWrite,
732
1060
  usePages,
733
1061
  useQueue,
1062
+ useSubscription,
1063
+ useSSE,
734
1064
  ...instanceApis
735
1065
  };
736
1066
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/react",
3
- "version": "0.12.0",
3
+ "version": "0.13.1-beta.0",
4
4
  "license": "MIT",
5
5
  "description": "React hooks for Spoosh API toolkit",
6
6
  "keywords": [
@@ -35,12 +35,19 @@
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@spoosh/core": ">=0.15.0",
38
+ "@spoosh/transport-sse": ">=0.1.0",
38
39
  "react": "^18 || ^19"
39
40
  },
41
+ "peerDependenciesMeta": {
42
+ "@spoosh/transport-sse": {
43
+ "optional": true
44
+ }
45
+ },
40
46
  "devDependencies": {
41
47
  "@testing-library/react": "^16.0.0",
42
48
  "jsdom": "^26.0.0",
43
- "@spoosh/core": "0.15.0",
49
+ "@spoosh/core": "0.16.0",
50
+ "@spoosh/transport-sse": "0.1.1",
44
51
  "@spoosh/test-utils": "0.3.0"
45
52
  },
46
53
  "scripts": {