@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/README.md +86 -1
- package/dist/index.d.mts +223 -21
- package/dist/index.d.ts +223 -21
- package/dist/index.js +320 -5
- package/dist/index.mjs +329 -5
- package/package.json +9 -2
package/dist/index.mjs
CHANGED
|
@@ -675,32 +675,354 @@ 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
|
+
import { resolveParser, resolveAccumulator } from "@spoosh/transport-sse";
|
|
809
|
+
function isSSETransport(transport) {
|
|
810
|
+
return "createSubscriptionAdapter" in transport && typeof transport.createSubscriptionAdapter === "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 = resolveParser(parseRef.current, data.event);
|
|
899
|
+
let parsed;
|
|
900
|
+
try {
|
|
901
|
+
parsed = parser(data.data);
|
|
902
|
+
} catch {
|
|
903
|
+
parsed = data.data;
|
|
904
|
+
}
|
|
905
|
+
if (parsed === void 0) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const accumulator = resolveAccumulator(accumulateRef.current, data.event);
|
|
909
|
+
const parsedObj = parsed;
|
|
910
|
+
const messageIndex = typeof parsedObj?.index === "number" ? parsedObj.index : void 0;
|
|
911
|
+
if (messageIndex !== void 0) {
|
|
912
|
+
const lastIndex = lastMessageIndexRef.current[data.event];
|
|
913
|
+
if (lastIndex !== void 0 && messageIndex < lastIndex) {
|
|
914
|
+
setAccumulatedData({});
|
|
915
|
+
lastMessageIndexRef.current = {};
|
|
916
|
+
}
|
|
917
|
+
lastMessageIndexRef.current[data.event] = messageIndex;
|
|
918
|
+
}
|
|
919
|
+
setAccumulatedData((prev) => {
|
|
920
|
+
const previousEventData = prev[data.event];
|
|
921
|
+
let newEventData;
|
|
922
|
+
try {
|
|
923
|
+
newEventData = accumulator(previousEventData, parsed);
|
|
924
|
+
} catch {
|
|
925
|
+
newEventData = parsed;
|
|
926
|
+
}
|
|
927
|
+
const newAccumulated = {
|
|
928
|
+
...prev,
|
|
929
|
+
[data.event]: newEventData
|
|
930
|
+
};
|
|
931
|
+
eventEmitter?.emit(
|
|
932
|
+
"spoosh:subscription:accumulate",
|
|
933
|
+
{
|
|
934
|
+
queryKey: subscription._queryKey,
|
|
935
|
+
eventType: data.event,
|
|
936
|
+
accumulatedData: newAccumulated,
|
|
937
|
+
timestamp: Date.now()
|
|
938
|
+
}
|
|
939
|
+
);
|
|
940
|
+
return newAccumulated;
|
|
941
|
+
});
|
|
942
|
+
}, [subscription.data, subscription._queryKey, eventSet]);
|
|
943
|
+
const reset = useCallback5(() => {
|
|
944
|
+
setAccumulatedData({});
|
|
945
|
+
}, []);
|
|
946
|
+
const trigger = useCallback5(
|
|
947
|
+
async (opts) => {
|
|
948
|
+
reset();
|
|
949
|
+
const triggerOpts = {
|
|
950
|
+
...opts ?? {},
|
|
951
|
+
maxRetries: optionsRef.current.maxRetries,
|
|
952
|
+
retryDelay: optionsRef.current.retryDelay
|
|
953
|
+
};
|
|
954
|
+
currentOptionsRef.current = {
|
|
955
|
+
...currentOptionsRef.current,
|
|
956
|
+
...triggerOpts
|
|
957
|
+
};
|
|
958
|
+
await subscription.trigger(triggerOpts);
|
|
959
|
+
},
|
|
960
|
+
[subscription.trigger, reset]
|
|
961
|
+
);
|
|
962
|
+
return {
|
|
963
|
+
data: Object.keys(accumulatedData).length ? accumulatedData : void 0,
|
|
964
|
+
error: subscription.error,
|
|
965
|
+
isConnected: subscription.isConnected,
|
|
966
|
+
loading: subscription.loading,
|
|
967
|
+
meta: {},
|
|
968
|
+
trigger,
|
|
969
|
+
disconnect: subscription.disconnect,
|
|
970
|
+
reset
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
return useSSE;
|
|
974
|
+
}
|
|
975
|
+
|
|
678
976
|
// src/create/index.ts
|
|
679
977
|
function create(instance) {
|
|
680
|
-
const { api, stateManager, eventEmitter, pluginExecutor } = instance;
|
|
978
|
+
const { api, stateManager, eventEmitter, pluginExecutor, transports } = instance;
|
|
681
979
|
const useRead = createUseRead({
|
|
682
980
|
api,
|
|
683
981
|
stateManager,
|
|
684
982
|
eventEmitter,
|
|
685
|
-
pluginExecutor
|
|
983
|
+
pluginExecutor,
|
|
984
|
+
transports,
|
|
985
|
+
config: instance.config
|
|
686
986
|
});
|
|
687
987
|
const useWrite = createUseWrite({
|
|
688
988
|
api,
|
|
689
989
|
stateManager,
|
|
690
990
|
eventEmitter,
|
|
691
|
-
pluginExecutor
|
|
991
|
+
pluginExecutor,
|
|
992
|
+
transports,
|
|
993
|
+
config: instance.config
|
|
692
994
|
});
|
|
693
995
|
const usePages = createUsePages({
|
|
694
996
|
api,
|
|
695
997
|
stateManager,
|
|
696
998
|
eventEmitter,
|
|
697
|
-
pluginExecutor
|
|
999
|
+
pluginExecutor,
|
|
1000
|
+
transports,
|
|
1001
|
+
config: instance.config
|
|
698
1002
|
});
|
|
699
1003
|
const useQueue = createUseQueue({
|
|
700
1004
|
api,
|
|
701
1005
|
stateManager,
|
|
702
1006
|
eventEmitter,
|
|
703
|
-
pluginExecutor
|
|
1007
|
+
pluginExecutor,
|
|
1008
|
+
transports,
|
|
1009
|
+
config: instance.config
|
|
1010
|
+
});
|
|
1011
|
+
const useSubscription = createUseSubscription({
|
|
1012
|
+
api,
|
|
1013
|
+
stateManager,
|
|
1014
|
+
eventEmitter,
|
|
1015
|
+
pluginExecutor,
|
|
1016
|
+
transports,
|
|
1017
|
+
config: instance.config
|
|
1018
|
+
});
|
|
1019
|
+
const useSSE = createUseSSE({
|
|
1020
|
+
api,
|
|
1021
|
+
stateManager,
|
|
1022
|
+
eventEmitter,
|
|
1023
|
+
pluginExecutor,
|
|
1024
|
+
transports,
|
|
1025
|
+
config: instance.config
|
|
704
1026
|
});
|
|
705
1027
|
const plugins = pluginExecutor.getPlugins();
|
|
706
1028
|
const setupContext = {
|
|
@@ -731,6 +1053,8 @@ function create(instance) {
|
|
|
731
1053
|
useWrite,
|
|
732
1054
|
usePages,
|
|
733
1055
|
useQueue,
|
|
1056
|
+
useSubscription,
|
|
1057
|
+
useSSE,
|
|
734
1058
|
...instanceApis
|
|
735
1059
|
};
|
|
736
1060
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spoosh/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.0",
|
|
44
51
|
"@spoosh/test-utils": "0.3.0"
|
|
45
52
|
},
|
|
46
53
|
"scripts": {
|