@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/README.md +86 -1
- package/dist/index.d.mts +223 -21
- package/dist/index.d.ts +223 -21
- package/dist/index.js +326 -5
- package/dist/index.mjs +335 -5
- package/package.json +9 -2
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.
|
|
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.
|
|
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": {
|