@spoosh/react 0.15.1 → 0.15.3

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.d.mts CHANGED
@@ -71,6 +71,9 @@ type ExtractAllSubscriptionEvents<T> = SubscriptionReturnType<T> extends {
71
71
  type ExtractSubscriptionError<T> = SubscriptionReturnType<T> extends {
72
72
  error: infer E;
73
73
  } ? E : unknown;
74
+ type ExtractSubscriptionParamNames<T> = SubscriptionReturnType<T> extends {
75
+ params: infer P;
76
+ } ? P extends Record<infer K, string | number> ? K extends string ? K : never : never : never;
74
77
 
75
78
  /**
76
79
  * Base options for `useRead` hook.
@@ -476,7 +479,31 @@ interface UseSSEOptions extends UseSSEOptionsBase {
476
479
  /** Accumulate strategy for combining events. Defaults to 'replace'. */
477
480
  accumulate?: AccumulateStrategy | Record<string, AccumulateStrategy | AccumulatorFn>;
478
481
  }
479
- interface UseSSEResult<TEvents, TError> {
482
+ type SSEReturnType<T> = T extends (...args: never[]) => infer R ? R : never;
483
+ type ExtractSSETriggerQuery<R> = R extends {
484
+ query: infer Q;
485
+ } ? {
486
+ query?: Q;
487
+ } : unknown;
488
+ type ExtractSSETriggerBody<R> = R extends {
489
+ body: infer B;
490
+ } ? {
491
+ body?: B;
492
+ } : unknown;
493
+ type ExtractSSETriggerParams<R> = R extends {
494
+ params: infer P;
495
+ } ? {
496
+ params?: P;
497
+ } : unknown;
498
+ type SSETriggerOptionsFromFn<TSubFn> = SSEReturnType<TSubFn> extends infer R ? ExtractSSETriggerQuery<R> & ExtractSSETriggerBody<R> & ExtractSSETriggerParams<R> : object;
499
+ type SSETriggerOptions<TQuery, TBody, TParams> = (TQuery extends never ? unknown : {
500
+ query?: TQuery;
501
+ }) & (TBody extends never ? unknown : {
502
+ body?: TBody;
503
+ }) & (TParams extends never ? unknown : {
504
+ params?: TParams;
505
+ });
506
+ interface UseSSEResultBase<TEvents, TError> {
480
507
  /** Accumulated data keyed by event type */
481
508
  data: Partial<TEvents> | undefined;
482
509
  /** Connection or parse error */
@@ -487,19 +514,18 @@ interface UseSSEResult<TEvents, TError> {
487
514
  loading: boolean;
488
515
  /** Plugin metadata */
489
516
  meta: Record<string, never>;
490
- /**
491
- * Manually trigger connection with optional body/query overrides
492
- * @param options - Optional body and query parameters
493
- */
494
- trigger: (options?: {
495
- body?: unknown;
496
- query?: unknown;
497
- }) => Promise<void>;
498
517
  /** Disconnect from the SSE endpoint */
499
518
  disconnect: () => void;
500
519
  /** Reset accumulated data */
501
520
  reset: () => void;
502
521
  }
522
+ type UseSSEResult<TEvents, TError, TSubFn> = UseSSEResultBase<TEvents, TError> & {
523
+ /**
524
+ * Manually trigger connection with optional body/query/params overrides
525
+ * @param options - Optional body, query, and params parameters
526
+ */
527
+ trigger: (options?: SSETriggerOptionsFromFn<TSubFn>) => Promise<void>;
528
+ };
503
529
 
504
530
  type InferError<T, TDefaultError> = unknown extends T ? TDefaultError : T;
505
531
  type WriteResolverContext<TSchema, TMethod, TDefaultError> = ResolverContext<TSchema, ExtractMethodData<TMethod>, InferError<ExtractMethodError<TMethod>, TDefaultError>, ExtractMethodQuery<TMethod>, ExtractMethodBody<TMethod>, ExtractResponseParamNames<TMethod> extends never ? never : Record<ExtractResponseParamNames<TMethod>, string | number>>;
@@ -535,13 +561,13 @@ type UseSSEFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
535
561
  }>;
536
562
  }, const TSelectedEvents extends readonly Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>[]>(subFn: TSubFn, sseOptions: TypedUseSSEOptions<Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>, InferSSEEvents<TSubFn>, TSelectedEvents> & {
537
563
  events: TSelectedEvents;
538
- }): UseSSEResult<FilteredEvents<InferSSEEvents<TSubFn>, TSelectedEvents>, InferError<ExtractMethodError<TSubFn>, TDefaultError>>;
564
+ }): UseSSEResult<FilteredEvents<InferSSEEvents<TSubFn>, TSelectedEvents>, InferError<ExtractMethodError<TSubFn>, TDefaultError>, TSubFn>;
539
565
  <TSubFn extends (api: SubscriptionApiClient<TSchema, TDefaultError>) => {
540
566
  _subscription: true;
541
567
  events: Record<string, {
542
568
  data: unknown;
543
569
  }>;
544
- }>(subFn: TSubFn, sseOptions?: Omit<TypedUseSSEOptions<Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>, InferSSEEvents<TSubFn>>, "events">): UseSSEResult<InferSSEEvents<TSubFn>, InferError<ExtractMethodError<TSubFn>, TDefaultError>>;
570
+ }>(subFn: TSubFn, sseOptions?: Omit<TypedUseSSEOptions<Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>, InferSSEEvents<TSubFn>>, "events">): UseSSEResult<InferSSEEvents<TSubFn>, InferError<ExtractMethodError<TSubFn>, TDefaultError>, TSubFn>;
545
571
  };
546
572
  /**
547
573
  * Base hooks that are always available.
@@ -737,4 +763,4 @@ type PluginHooksConfig<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[
737
763
  plugins: TPlugins;
738
764
  };
739
765
 
740
- export { type ApiClient, type BasePagesOptions, type BasePagesResult, type BaseReadOptions, type BaseReadResult, type BaseSubscriptionOptions, type BaseSubscriptionResult, type BaseWriteResult, type ExtractAllSubscriptionEventKeys, type ExtractAllSubscriptionEvents, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type ExtractSubscriptionBody, type ExtractSubscriptionError, type ExtractSubscriptionEvents, type ExtractSubscriptionQuery, type PagesApiClient, type PagesTriggerOptions, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReactOptionsMap, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type SubscriptionApiClient, type SubscriptionTriggerInput, type TriggerOptions, type TypedAccumulateConfig, type TypedParseConfig, type TypedUseSSEOptions, type UsePagesResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseSSEOptions, type UseSSEOptionsBase, type UseSSEResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, type WriteTriggerInput, create };
766
+ export { type ApiClient, type BasePagesOptions, type BasePagesResult, type BaseReadOptions, type BaseReadResult, type BaseSubscriptionOptions, type BaseSubscriptionResult, type BaseWriteResult, type ExtractAllSubscriptionEventKeys, type ExtractAllSubscriptionEvents, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type ExtractSubscriptionBody, type ExtractSubscriptionError, type ExtractSubscriptionEvents, type ExtractSubscriptionParamNames, type ExtractSubscriptionQuery, type PagesApiClient, type PagesTriggerOptions, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReactOptionsMap, type ReadApiClient, type ResponseInputFields, type SSETriggerOptions, type SSETriggerOptionsFromFn, type SpooshReactHooks, type SubscriptionApiClient, type SubscriptionTriggerInput, type TriggerOptions, type TypedAccumulateConfig, type TypedParseConfig, type TypedUseSSEOptions, type UsePagesResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseSSEOptions, type UseSSEOptionsBase, type UseSSEResult, type UseSSEResultBase, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, type WriteTriggerInput, create };
package/dist/index.d.ts CHANGED
@@ -71,6 +71,9 @@ type ExtractAllSubscriptionEvents<T> = SubscriptionReturnType<T> extends {
71
71
  type ExtractSubscriptionError<T> = SubscriptionReturnType<T> extends {
72
72
  error: infer E;
73
73
  } ? E : unknown;
74
+ type ExtractSubscriptionParamNames<T> = SubscriptionReturnType<T> extends {
75
+ params: infer P;
76
+ } ? P extends Record<infer K, string | number> ? K extends string ? K : never : never : never;
74
77
 
75
78
  /**
76
79
  * Base options for `useRead` hook.
@@ -476,7 +479,31 @@ interface UseSSEOptions extends UseSSEOptionsBase {
476
479
  /** Accumulate strategy for combining events. Defaults to 'replace'. */
477
480
  accumulate?: AccumulateStrategy | Record<string, AccumulateStrategy | AccumulatorFn>;
478
481
  }
479
- interface UseSSEResult<TEvents, TError> {
482
+ type SSEReturnType<T> = T extends (...args: never[]) => infer R ? R : never;
483
+ type ExtractSSETriggerQuery<R> = R extends {
484
+ query: infer Q;
485
+ } ? {
486
+ query?: Q;
487
+ } : unknown;
488
+ type ExtractSSETriggerBody<R> = R extends {
489
+ body: infer B;
490
+ } ? {
491
+ body?: B;
492
+ } : unknown;
493
+ type ExtractSSETriggerParams<R> = R extends {
494
+ params: infer P;
495
+ } ? {
496
+ params?: P;
497
+ } : unknown;
498
+ type SSETriggerOptionsFromFn<TSubFn> = SSEReturnType<TSubFn> extends infer R ? ExtractSSETriggerQuery<R> & ExtractSSETriggerBody<R> & ExtractSSETriggerParams<R> : object;
499
+ type SSETriggerOptions<TQuery, TBody, TParams> = (TQuery extends never ? unknown : {
500
+ query?: TQuery;
501
+ }) & (TBody extends never ? unknown : {
502
+ body?: TBody;
503
+ }) & (TParams extends never ? unknown : {
504
+ params?: TParams;
505
+ });
506
+ interface UseSSEResultBase<TEvents, TError> {
480
507
  /** Accumulated data keyed by event type */
481
508
  data: Partial<TEvents> | undefined;
482
509
  /** Connection or parse error */
@@ -487,19 +514,18 @@ interface UseSSEResult<TEvents, TError> {
487
514
  loading: boolean;
488
515
  /** Plugin metadata */
489
516
  meta: Record<string, never>;
490
- /**
491
- * Manually trigger connection with optional body/query overrides
492
- * @param options - Optional body and query parameters
493
- */
494
- trigger: (options?: {
495
- body?: unknown;
496
- query?: unknown;
497
- }) => Promise<void>;
498
517
  /** Disconnect from the SSE endpoint */
499
518
  disconnect: () => void;
500
519
  /** Reset accumulated data */
501
520
  reset: () => void;
502
521
  }
522
+ type UseSSEResult<TEvents, TError, TSubFn> = UseSSEResultBase<TEvents, TError> & {
523
+ /**
524
+ * Manually trigger connection with optional body/query/params overrides
525
+ * @param options - Optional body, query, and params parameters
526
+ */
527
+ trigger: (options?: SSETriggerOptionsFromFn<TSubFn>) => Promise<void>;
528
+ };
503
529
 
504
530
  type InferError<T, TDefaultError> = unknown extends T ? TDefaultError : T;
505
531
  type WriteResolverContext<TSchema, TMethod, TDefaultError> = ResolverContext<TSchema, ExtractMethodData<TMethod>, InferError<ExtractMethodError<TMethod>, TDefaultError>, ExtractMethodQuery<TMethod>, ExtractMethodBody<TMethod>, ExtractResponseParamNames<TMethod> extends never ? never : Record<ExtractResponseParamNames<TMethod>, string | number>>;
@@ -535,13 +561,13 @@ type UseSSEFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
535
561
  }>;
536
562
  }, const TSelectedEvents extends readonly Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>[]>(subFn: TSubFn, sseOptions: TypedUseSSEOptions<Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>, InferSSEEvents<TSubFn>, TSelectedEvents> & {
537
563
  events: TSelectedEvents;
538
- }): UseSSEResult<FilteredEvents<InferSSEEvents<TSubFn>, TSelectedEvents>, InferError<ExtractMethodError<TSubFn>, TDefaultError>>;
564
+ }): UseSSEResult<FilteredEvents<InferSSEEvents<TSubFn>, TSelectedEvents>, InferError<ExtractMethodError<TSubFn>, TDefaultError>, TSubFn>;
539
565
  <TSubFn extends (api: SubscriptionApiClient<TSchema, TDefaultError>) => {
540
566
  _subscription: true;
541
567
  events: Record<string, {
542
568
  data: unknown;
543
569
  }>;
544
- }>(subFn: TSubFn, sseOptions?: Omit<TypedUseSSEOptions<Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>, InferSSEEvents<TSubFn>>, "events">): UseSSEResult<InferSSEEvents<TSubFn>, InferError<ExtractMethodError<TSubFn>, TDefaultError>>;
570
+ }>(subFn: TSubFn, sseOptions?: Omit<TypedUseSSEOptions<Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>, InferSSEEvents<TSubFn>>, "events">): UseSSEResult<InferSSEEvents<TSubFn>, InferError<ExtractMethodError<TSubFn>, TDefaultError>, TSubFn>;
545
571
  };
546
572
  /**
547
573
  * Base hooks that are always available.
@@ -737,4 +763,4 @@ type PluginHooksConfig<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[
737
763
  plugins: TPlugins;
738
764
  };
739
765
 
740
- export { type ApiClient, type BasePagesOptions, type BasePagesResult, type BaseReadOptions, type BaseReadResult, type BaseSubscriptionOptions, type BaseSubscriptionResult, type BaseWriteResult, type ExtractAllSubscriptionEventKeys, type ExtractAllSubscriptionEvents, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type ExtractSubscriptionBody, type ExtractSubscriptionError, type ExtractSubscriptionEvents, type ExtractSubscriptionQuery, type PagesApiClient, type PagesTriggerOptions, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReactOptionsMap, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type SubscriptionApiClient, type SubscriptionTriggerInput, type TriggerOptions, type TypedAccumulateConfig, type TypedParseConfig, type TypedUseSSEOptions, type UsePagesResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseSSEOptions, type UseSSEOptionsBase, type UseSSEResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, type WriteTriggerInput, create };
766
+ export { type ApiClient, type BasePagesOptions, type BasePagesResult, type BaseReadOptions, type BaseReadResult, type BaseSubscriptionOptions, type BaseSubscriptionResult, type BaseWriteResult, type ExtractAllSubscriptionEventKeys, type ExtractAllSubscriptionEvents, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type ExtractSubscriptionBody, type ExtractSubscriptionError, type ExtractSubscriptionEvents, type ExtractSubscriptionParamNames, type ExtractSubscriptionQuery, type PagesApiClient, type PagesTriggerOptions, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReactOptionsMap, type ReadApiClient, type ResponseInputFields, type SSETriggerOptions, type SSETriggerOptionsFromFn, type SpooshReactHooks, type SubscriptionApiClient, type SubscriptionTriggerInput, type TriggerOptions, type TypedAccumulateConfig, type TypedParseConfig, type TypedUseSSEOptions, type UsePagesResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseSSEOptions, type UseSSEOptionsBase, type UseSSEResult, type UseSSEResultBase, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, type WriteTriggerInput, create };
package/dist/index.js CHANGED
@@ -688,11 +688,9 @@ function createUseSubscription(options) {
688
688
  });
689
689
  const controllerRef = (0, import_react5.useRef)(null);
690
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)({
691
+ const queryKeyChanged = controllerRef.current && controllerRef.current.queryKey !== queryKey;
692
+ if (!controllerRef.current || queryKeyChanged) {
693
+ const controller2 = (0, import_core5.createSubscriptionController)({
696
694
  channel: capturedCall.path,
697
695
  baseAdapter: adapter,
698
696
  stateManager,
@@ -703,33 +701,18 @@ function createUseSubscription(options) {
703
701
  path: capturedCall.path,
704
702
  method: capturedCall.method
705
703
  });
706
- controllerRef.current = controller;
707
- return controller;
708
- }, [
709
- queryKey,
710
- adapter,
711
- operationType,
712
- capturedCall.path,
713
- capturedCall.method
714
- ]);
704
+ controllerRef.current = { controller: controller2, queryKey };
705
+ }
706
+ const controller = controllerRef.current.controller;
715
707
  const subscribe = (0, import_react5.useCallback)(
716
708
  (callback) => {
717
- const controller = getOrCreateController();
718
709
  return controller.subscribe(callback);
719
710
  },
720
- [getOrCreateController]
711
+ [controller]
721
712
  );
722
- const emptyStateRef = (0, import_react5.useRef)({
723
- data: void 0,
724
- error: void 0,
725
- isConnected: false
726
- });
727
713
  const getSnapshot = (0, import_react5.useCallback)(() => {
728
- if (!controllerRef.current) {
729
- return emptyStateRef.current;
730
- }
731
- return controllerRef.current.getState();
732
- }, []);
714
+ return controller.getState();
715
+ }, [controller]);
733
716
  const state = (0, import_react5.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
734
717
  const [isPending, setIsPending] = (0, import_react5.useState)(enabled);
735
718
  (0, import_react5.useEffect)(() => {
@@ -737,14 +720,13 @@ function createUseSubscription(options) {
737
720
  return;
738
721
  }
739
722
  setIsPending(true);
740
- const controller = getOrCreateController();
741
723
  controller.mount();
742
724
  controller.subscribe();
743
725
  return () => {
744
726
  subscriptionVersionRef.current++;
745
727
  controller.unsubscribe();
746
728
  };
747
- }, [queryKey, enabled, getOrCreateController]);
729
+ }, [queryKey, enabled, controller]);
748
730
  (0, import_react5.useEffect)(() => {
749
731
  if (state.isConnected || state.data !== void 0 || state.error !== void 0) {
750
732
  setIsPending(false);
@@ -752,19 +734,17 @@ function createUseSubscription(options) {
752
734
  }, [state.isConnected, state.data, state.error]);
753
735
  const disconnect = (0, import_react5.useCallback)(() => {
754
736
  subscriptionVersionRef.current++;
755
- if (controllerRef.current) {
756
- controllerRef.current.unsubscribe();
757
- }
758
- }, []);
737
+ controller.unsubscribe();
738
+ }, [controller]);
759
739
  const trigger = (0, import_react5.useCallback)(async () => {
760
740
  setIsPending(true);
761
741
  subscriptionVersionRef.current++;
762
- const controller = getOrCreateController();
763
742
  controller.unsubscribe();
764
743
  controller.mount();
765
744
  await controller.subscribe();
766
- }, [getOrCreateController]);
745
+ }, [controller]);
767
746
  const loading = isPending;
747
+ const stateWithQueue = state;
768
748
  return {
769
749
  meta: {},
770
750
  data: state.data,
@@ -773,6 +753,8 @@ function createUseSubscription(options) {
773
753
  isConnected: state.isConnected,
774
754
  _queryKey: queryKey,
775
755
  _subscriptionVersion: subscriptionVersionRef.current,
756
+ _messageQueue: stateWithQueue._messageQueue,
757
+ _queueIndex: stateWithQueue._queueIndex ?? 0,
776
758
  trigger,
777
759
  disconnect
778
760
  };
@@ -822,12 +804,19 @@ function createUseSSE(options) {
822
804
  if (!capturedCall) {
823
805
  throw new Error("useSSE requires calling a method");
824
806
  }
807
+ const requestOptions = capturedCall.options;
808
+ const resolvedPath = (0, import_core6.resolvePathString)(
809
+ capturedCall.path,
810
+ requestOptions?.params
811
+ );
812
+ const paramsKey = requestOptions?.params ? JSON.stringify(requestOptions.params) : "";
825
813
  const currentOptionsRef = (0, import_react6.useRef)(
826
814
  capturedCall.options
827
815
  );
816
+ currentOptionsRef.current = capturedCall.options;
828
817
  const adapter = (0, import_react6.useMemo)(
829
818
  () => transport.createSubscriptionAdapter({
830
- channel: capturedCall.path,
819
+ channel: resolvedPath,
831
820
  method: capturedCall.method,
832
821
  baseUrl: config.baseUrl,
833
822
  globalHeaders: config.defaultOptions.headers,
@@ -835,7 +824,7 @@ function createUseSSE(options) {
835
824
  eventEmitter,
836
825
  devtoolMeta: events ? { listenedEvents: events } : void 0
837
826
  }),
838
- [capturedCall.path, capturedCall.method]
827
+ [resolvedPath, capturedCall.method, paramsKey]
839
828
  );
840
829
  const [accumulatedData, setAccumulatedData] = (0, import_react6.useState)({});
841
830
  const eventSet = (0, import_react6.useMemo)(
@@ -858,76 +847,87 @@ function createUseSSE(options) {
858
847
  });
859
848
  const prevVersionRef = (0, import_react6.useRef)(subscription._subscriptionVersion);
860
849
  const lastMessageIndexRef = (0, import_react6.useRef)({});
850
+ const lastProcessedQueueIndexRef = (0, import_react6.useRef)(0);
861
851
  (0, import_react6.useEffect)(() => {
862
852
  if (subscription._subscriptionVersion !== prevVersionRef.current) {
863
853
  setAccumulatedData({});
864
854
  lastMessageIndexRef.current = {};
855
+ lastProcessedQueueIndexRef.current = 0;
865
856
  }
866
857
  prevVersionRef.current = subscription._subscriptionVersion;
867
858
  }, [subscription._subscriptionVersion]);
859
+ const subscriptionState = subscription;
860
+ const messageQueue = subscriptionState._messageQueue ?? [];
861
+ const queueIndex = subscriptionState._queueIndex ?? 0;
868
862
  (0, import_react6.useEffect)(() => {
869
- const data = subscription.data;
870
- if (!data) {
863
+ if (queueIndex <= lastProcessedQueueIndexRef.current) {
871
864
  return;
872
865
  }
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 = {};
866
+ const startIndex = lastProcessedQueueIndexRef.current;
867
+ const messagesToProcess = messageQueue.slice(startIndex);
868
+ lastProcessedQueueIndexRef.current = queueIndex;
869
+ for (const data of messagesToProcess) {
870
+ if (!data) continue;
871
+ if (eventSet && !eventSet.has(data.event)) {
872
+ continue;
900
873
  }
901
- lastMessageIndexRef.current[data.event] = messageIndex;
902
- }
903
- setAccumulatedData((prev) => {
904
- const previousEventData = prev[data.event];
905
- let newEventData;
874
+ const parser = transport.utils.resolveParser(
875
+ parseRef.current,
876
+ data.event
877
+ );
878
+ let parsed;
906
879
  try {
907
- newEventData = accumulator(previousEventData, parsed);
880
+ parsed = parser(data.data);
908
881
  } catch {
909
- newEventData = parsed;
882
+ parsed = data.data;
910
883
  }
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
- }
884
+ if (parsed === void 0) {
885
+ continue;
886
+ }
887
+ const accumulator = transport.utils.resolveAccumulator(
888
+ accumulateRef.current,
889
+ data.event
923
890
  );
924
- return newAccumulated;
925
- });
926
- }, [subscription.data, subscription._queryKey, eventSet]);
891
+ const parsedObj = parsed;
892
+ const messageIndex = typeof parsedObj?.index === "number" ? parsedObj.index : void 0;
893
+ if (messageIndex !== void 0) {
894
+ const lastIndex = lastMessageIndexRef.current[data.event];
895
+ if (lastIndex !== void 0 && messageIndex < lastIndex) {
896
+ setAccumulatedData({});
897
+ lastMessageIndexRef.current = {};
898
+ }
899
+ lastMessageIndexRef.current[data.event] = messageIndex;
900
+ }
901
+ setAccumulatedData((prev) => {
902
+ const previousEventData = prev[data.event];
903
+ let newEventData;
904
+ try {
905
+ newEventData = accumulator(previousEventData, parsed);
906
+ } catch {
907
+ newEventData = parsed;
908
+ }
909
+ const newAccumulated = {
910
+ ...prev,
911
+ [data.event]: newEventData
912
+ };
913
+ eventEmitter?.emit(
914
+ "spoosh:subscription:accumulate",
915
+ {
916
+ queryKey: subscription._queryKey,
917
+ eventType: data.event,
918
+ accumulatedData: newAccumulated,
919
+ timestamp: Date.now()
920
+ }
921
+ );
922
+ return newAccumulated;
923
+ });
924
+ }
925
+ }, [queueIndex, subscription._queryKey, eventSet]);
927
926
  const reset = (0, import_react6.useCallback)(() => {
928
927
  setAccumulatedData({});
929
928
  }, []);
930
929
  const trigger = (0, import_react6.useCallback)(
930
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
931
931
  async (opts) => {
932
932
  reset();
933
933
  const triggerOpts = {
package/dist/index.mjs CHANGED
@@ -712,11 +712,9 @@ function createUseSubscription(options) {
712
712
  });
713
713
  const controllerRef = useRef5(null);
714
714
  const subscriptionVersionRef = useRef5(0);
715
- const getOrCreateController = useCallback4(() => {
716
- if (controllerRef.current) {
717
- return controllerRef.current;
718
- }
719
- const controller = createSubscriptionController({
715
+ const queryKeyChanged = controllerRef.current && controllerRef.current.queryKey !== queryKey;
716
+ if (!controllerRef.current || queryKeyChanged) {
717
+ const controller2 = createSubscriptionController({
720
718
  channel: capturedCall.path,
721
719
  baseAdapter: adapter,
722
720
  stateManager,
@@ -727,33 +725,18 @@ function createUseSubscription(options) {
727
725
  path: capturedCall.path,
728
726
  method: capturedCall.method
729
727
  });
730
- controllerRef.current = controller;
731
- return controller;
732
- }, [
733
- queryKey,
734
- adapter,
735
- operationType,
736
- capturedCall.path,
737
- capturedCall.method
738
- ]);
728
+ controllerRef.current = { controller: controller2, queryKey };
729
+ }
730
+ const controller = controllerRef.current.controller;
739
731
  const subscribe = useCallback4(
740
732
  (callback) => {
741
- const controller = getOrCreateController();
742
733
  return controller.subscribe(callback);
743
734
  },
744
- [getOrCreateController]
735
+ [controller]
745
736
  );
746
- const emptyStateRef = useRef5({
747
- data: void 0,
748
- error: void 0,
749
- isConnected: false
750
- });
751
737
  const getSnapshot = useCallback4(() => {
752
- if (!controllerRef.current) {
753
- return emptyStateRef.current;
754
- }
755
- return controllerRef.current.getState();
756
- }, []);
738
+ return controller.getState();
739
+ }, [controller]);
757
740
  const state = useSyncExternalStore5(subscribe, getSnapshot, getSnapshot);
758
741
  const [isPending, setIsPending] = useState4(enabled);
759
742
  useEffect4(() => {
@@ -761,14 +744,13 @@ function createUseSubscription(options) {
761
744
  return;
762
745
  }
763
746
  setIsPending(true);
764
- const controller = getOrCreateController();
765
747
  controller.mount();
766
748
  controller.subscribe();
767
749
  return () => {
768
750
  subscriptionVersionRef.current++;
769
751
  controller.unsubscribe();
770
752
  };
771
- }, [queryKey, enabled, getOrCreateController]);
753
+ }, [queryKey, enabled, controller]);
772
754
  useEffect4(() => {
773
755
  if (state.isConnected || state.data !== void 0 || state.error !== void 0) {
774
756
  setIsPending(false);
@@ -776,19 +758,17 @@ function createUseSubscription(options) {
776
758
  }, [state.isConnected, state.data, state.error]);
777
759
  const disconnect = useCallback4(() => {
778
760
  subscriptionVersionRef.current++;
779
- if (controllerRef.current) {
780
- controllerRef.current.unsubscribe();
781
- }
782
- }, []);
761
+ controller.unsubscribe();
762
+ }, [controller]);
783
763
  const trigger = useCallback4(async () => {
784
764
  setIsPending(true);
785
765
  subscriptionVersionRef.current++;
786
- const controller = getOrCreateController();
787
766
  controller.unsubscribe();
788
767
  controller.mount();
789
768
  await controller.subscribe();
790
- }, [getOrCreateController]);
769
+ }, [controller]);
791
770
  const loading = isPending;
771
+ const stateWithQueue = state;
792
772
  return {
793
773
  meta: {},
794
774
  data: state.data,
@@ -797,6 +777,8 @@ function createUseSubscription(options) {
797
777
  isConnected: state.isConnected,
798
778
  _queryKey: queryKey,
799
779
  _subscriptionVersion: subscriptionVersionRef.current,
780
+ _messageQueue: stateWithQueue._messageQueue,
781
+ _queueIndex: stateWithQueue._queueIndex ?? 0,
800
782
  trigger,
801
783
  disconnect
802
784
  };
@@ -806,7 +788,7 @@ function createUseSubscription(options) {
806
788
 
807
789
  // src/useSSE/index.ts
808
790
  import { useState as useState5, useRef as useRef6, useEffect as useEffect5, useCallback as useCallback5, useMemo } from "react";
809
- import { createSelectorProxy as createSelectorProxy6 } from "@spoosh/core";
791
+ import { createSelectorProxy as createSelectorProxy6, resolvePathString } from "@spoosh/core";
810
792
  function isSSETransport(transport) {
811
793
  const t = transport;
812
794
  return typeof t.createSubscriptionAdapter === "function" && typeof t.utils?.resolveParser === "function" && typeof t.utils?.resolveAccumulator === "function";
@@ -846,12 +828,19 @@ function createUseSSE(options) {
846
828
  if (!capturedCall) {
847
829
  throw new Error("useSSE requires calling a method");
848
830
  }
831
+ const requestOptions = capturedCall.options;
832
+ const resolvedPath = resolvePathString(
833
+ capturedCall.path,
834
+ requestOptions?.params
835
+ );
836
+ const paramsKey = requestOptions?.params ? JSON.stringify(requestOptions.params) : "";
849
837
  const currentOptionsRef = useRef6(
850
838
  capturedCall.options
851
839
  );
840
+ currentOptionsRef.current = capturedCall.options;
852
841
  const adapter = useMemo(
853
842
  () => transport.createSubscriptionAdapter({
854
- channel: capturedCall.path,
843
+ channel: resolvedPath,
855
844
  method: capturedCall.method,
856
845
  baseUrl: config.baseUrl,
857
846
  globalHeaders: config.defaultOptions.headers,
@@ -859,7 +848,7 @@ function createUseSSE(options) {
859
848
  eventEmitter,
860
849
  devtoolMeta: events ? { listenedEvents: events } : void 0
861
850
  }),
862
- [capturedCall.path, capturedCall.method]
851
+ [resolvedPath, capturedCall.method, paramsKey]
863
852
  );
864
853
  const [accumulatedData, setAccumulatedData] = useState5({});
865
854
  const eventSet = useMemo(
@@ -882,76 +871,87 @@ function createUseSSE(options) {
882
871
  });
883
872
  const prevVersionRef = useRef6(subscription._subscriptionVersion);
884
873
  const lastMessageIndexRef = useRef6({});
874
+ const lastProcessedQueueIndexRef = useRef6(0);
885
875
  useEffect5(() => {
886
876
  if (subscription._subscriptionVersion !== prevVersionRef.current) {
887
877
  setAccumulatedData({});
888
878
  lastMessageIndexRef.current = {};
879
+ lastProcessedQueueIndexRef.current = 0;
889
880
  }
890
881
  prevVersionRef.current = subscription._subscriptionVersion;
891
882
  }, [subscription._subscriptionVersion]);
883
+ const subscriptionState = subscription;
884
+ const messageQueue = subscriptionState._messageQueue ?? [];
885
+ const queueIndex = subscriptionState._queueIndex ?? 0;
892
886
  useEffect5(() => {
893
- const data = subscription.data;
894
- if (!data) {
887
+ if (queueIndex <= lastProcessedQueueIndexRef.current) {
895
888
  return;
896
889
  }
897
- if (eventSet && !eventSet.has(data.event)) {
898
- return;
899
- }
900
- const parser = transport.utils.resolveParser(
901
- parseRef.current,
902
- data.event
903
- );
904
- let parsed;
905
- try {
906
- parsed = parser(data.data);
907
- } catch {
908
- parsed = data.data;
909
- }
910
- if (parsed === void 0) {
911
- return;
912
- }
913
- const accumulator = transport.utils.resolveAccumulator(
914
- accumulateRef.current,
915
- data.event
916
- );
917
- const parsedObj = parsed;
918
- const messageIndex = typeof parsedObj?.index === "number" ? parsedObj.index : void 0;
919
- if (messageIndex !== void 0) {
920
- const lastIndex = lastMessageIndexRef.current[data.event];
921
- if (lastIndex !== void 0 && messageIndex < lastIndex) {
922
- setAccumulatedData({});
923
- lastMessageIndexRef.current = {};
890
+ const startIndex = lastProcessedQueueIndexRef.current;
891
+ const messagesToProcess = messageQueue.slice(startIndex);
892
+ lastProcessedQueueIndexRef.current = queueIndex;
893
+ for (const data of messagesToProcess) {
894
+ if (!data) continue;
895
+ if (eventSet && !eventSet.has(data.event)) {
896
+ continue;
924
897
  }
925
- lastMessageIndexRef.current[data.event] = messageIndex;
926
- }
927
- setAccumulatedData((prev) => {
928
- const previousEventData = prev[data.event];
929
- let newEventData;
898
+ const parser = transport.utils.resolveParser(
899
+ parseRef.current,
900
+ data.event
901
+ );
902
+ let parsed;
930
903
  try {
931
- newEventData = accumulator(previousEventData, parsed);
904
+ parsed = parser(data.data);
932
905
  } catch {
933
- newEventData = parsed;
906
+ parsed = data.data;
934
907
  }
935
- const newAccumulated = {
936
- ...prev,
937
- [data.event]: newEventData
938
- };
939
- eventEmitter?.emit(
940
- "spoosh:subscription:accumulate",
941
- {
942
- queryKey: subscription._queryKey,
943
- eventType: data.event,
944
- accumulatedData: newAccumulated,
945
- timestamp: Date.now()
946
- }
908
+ if (parsed === void 0) {
909
+ continue;
910
+ }
911
+ const accumulator = transport.utils.resolveAccumulator(
912
+ accumulateRef.current,
913
+ data.event
947
914
  );
948
- return newAccumulated;
949
- });
950
- }, [subscription.data, subscription._queryKey, eventSet]);
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
+ }
949
+ }, [queueIndex, subscription._queryKey, eventSet]);
951
950
  const reset = useCallback5(() => {
952
951
  setAccumulatedData({});
953
952
  }, []);
954
953
  const trigger = useCallback5(
954
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
955
955
  async (opts) => {
956
956
  reset();
957
957
  const triggerOpts = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/react",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "license": "MIT",
5
5
  "description": "React hooks for Spoosh API toolkit",
6
6
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  }
35
35
  },
36
36
  "peerDependencies": {
37
- "@spoosh/core": ">=0.18.2",
37
+ "@spoosh/core": ">=0.18.3",
38
38
  "@spoosh/transport-sse": ">=0.1.0",
39
39
  "react": "^18 || ^19"
40
40
  },
@@ -46,7 +46,7 @@
46
46
  "devDependencies": {
47
47
  "@testing-library/react": "^16.0.0",
48
48
  "jsdom": "^26.0.0",
49
- "@spoosh/core": "0.18.2",
49
+ "@spoosh/core": "0.18.3",
50
50
  "@spoosh/transport-sse": "0.1.2",
51
51
  "@spoosh/test-utils": "0.3.0"
52
52
  },