@fitzzero/quickdraw-core 3.9.1 → 4.1.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.
@@ -140,7 +140,9 @@ function QuickdrawProvider({
140
140
  authToken,
141
141
  autoConnect = !!authToken,
142
142
  withCredentials = true,
143
- socketPath
143
+ socketPath,
144
+ reconnectBehavior = "invalidate-queries",
145
+ transports
144
146
  }) {
145
147
  const [defaultQueryClient] = React.useState(() => createQueryClient());
146
148
  const actualQueryClient = queryClient ?? defaultQueryClient;
@@ -148,12 +150,40 @@ function QuickdrawProvider({
148
150
  const [isConnected, setIsConnected] = React.useState(false);
149
151
  const [userId, setUserId] = React.useState(null);
150
152
  const [serviceAccess, setServiceAccess] = React.useState({});
153
+ const [isRateLimited, setIsRateLimited] = React.useState(false);
151
154
  const subscriptionRegistryRef = React.useRef(createSubscriptionRegistry());
152
155
  const prevAuthTokenRef = React.useRef(authToken);
153
156
  const socketRef = React.useRef(null);
154
157
  const subscriptionBatcherRef = React.useRef(
155
158
  createSubscriptionBatcher(socketRef)
156
159
  );
160
+ const hasEverConnectedRef = React.useRef(false);
161
+ const reconnectBehaviorRef = React.useRef(reconnectBehavior);
162
+ reconnectBehaviorRef.current = reconnectBehavior;
163
+ const transportsRef = React.useRef(transports);
164
+ transportsRef.current = transports;
165
+ const queryClientRef = React.useRef(actualQueryClient);
166
+ queryClientRef.current = actualQueryClient;
167
+ const rateLimitUntilRef = React.useRef(0);
168
+ const rateLimitTimerRef = React.useRef(null);
169
+ const reportRateLimited = React.useCallback((retryAfterMs) => {
170
+ const backoffMs = Math.min(Math.max(retryAfterMs ?? 5e3, 250), 5 * 60 * 1e3);
171
+ const until = Date.now() + backoffMs;
172
+ if (until <= rateLimitUntilRef.current) return;
173
+ rateLimitUntilRef.current = until;
174
+ setIsRateLimited(true);
175
+ if (rateLimitTimerRef.current) clearTimeout(rateLimitTimerRef.current);
176
+ rateLimitTimerRef.current = setTimeout(() => {
177
+ rateLimitTimerRef.current = null;
178
+ rateLimitUntilRef.current = 0;
179
+ setIsRateLimited(false);
180
+ }, backoffMs);
181
+ }, []);
182
+ React.useEffect(() => {
183
+ return () => {
184
+ if (rateLimitTimerRef.current) clearTimeout(rateLimitTimerRef.current);
185
+ };
186
+ }, []);
157
187
  const connect = React.useCallback(
158
188
  (token) => {
159
189
  if (socketRef.current) return;
@@ -163,12 +193,16 @@ function QuickdrawProvider({
163
193
  const newSocket = io(serverUrl, {
164
194
  auth: authToUse ? { token: authToUse } : void 0,
165
195
  withCredentials,
166
- transports: ["websocket", "polling"],
196
+ transports: transportsRef.current ?? ["websocket", "polling"],
167
197
  autoConnect: true,
168
198
  ...socketPath ? { path: socketPath } : {}
169
199
  });
170
200
  newSocket.on("connect", () => {
171
201
  setIsConnected(true);
202
+ if (hasEverConnectedRef.current && reconnectBehaviorRef.current === "invalidate-queries") {
203
+ void queryClientRef.current.invalidateQueries();
204
+ }
205
+ hasEverConnectedRef.current = true;
172
206
  });
173
207
  newSocket.on("disconnect", () => {
174
208
  setIsConnected(false);
@@ -178,16 +212,21 @@ function QuickdrawProvider({
178
212
  "auth:info",
179
213
  (info) => {
180
214
  setUserId(info.userId);
181
- setServiceAccess(info.serviceAccess);
215
+ setServiceAccess(info.serviceAccess ?? {});
182
216
  }
183
217
  );
218
+ newSocket.on("error", (payload) => {
219
+ if (payload && payload.code === "RATE_LIMITED") {
220
+ reportRateLimited(payload.retryAfter);
221
+ }
222
+ });
184
223
  newSocket.on("connect_error", (error) => {
185
224
  console.error("Socket connection error:", error.message);
186
225
  });
187
226
  socketRef.current = newSocket;
188
227
  setSocket(newSocket);
189
228
  },
190
- [serverUrl, withCredentials, socketPath]
229
+ [serverUrl, withCredentials, socketPath, reportRateLimited]
191
230
  );
192
231
  const disconnect = React.useCallback(() => {
193
232
  if (socketRef.current) {
@@ -233,9 +272,20 @@ function QuickdrawProvider({
233
272
  connect,
234
273
  disconnect,
235
274
  subscriptionRegistry: subscriptionRegistryRef.current,
236
- subscriptionBatcher: subscriptionBatcherRef.current
275
+ subscriptionBatcher: subscriptionBatcherRef.current,
276
+ isRateLimited,
277
+ reportRateLimited
237
278
  }),
238
- [socket, isConnected, userId, serviceAccess, connect, disconnect]
279
+ [
280
+ socket,
281
+ isConnected,
282
+ userId,
283
+ serviceAccess,
284
+ connect,
285
+ disconnect,
286
+ isRateLimited,
287
+ reportRateLimited
288
+ ]
239
289
  );
240
290
  return /* @__PURE__ */ jsx(QueryClientProvider, { client: actualQueryClient, children: /* @__PURE__ */ jsx(QuickdrawSocketContext.Provider, { value: contextValue, children }) });
241
291
  }
@@ -357,8 +407,20 @@ function useServiceMethod(serviceName, methodName, options) {
357
407
  [execute, loading, error, data, socket, isConnected]
358
408
  );
359
409
  }
410
+
411
+ // src/client/serviceError.ts
412
+ var ServiceCallError = class extends Error {
413
+ code;
414
+ constructor(message, code) {
415
+ super(message);
416
+ this.name = "ServiceCallError";
417
+ this.code = code;
418
+ }
419
+ };
420
+
421
+ // src/client/useServiceQuery.ts
360
422
  function useServiceQuery(serviceName, methodName, payload, options) {
361
- const { socket, isConnected } = useQuickdrawSocket();
423
+ const { socket, isConnected, isRateLimited, reportRateLimited } = useQuickdrawSocket();
362
424
  const queryClient = useQueryClient();
363
425
  const [error, setError] = React.useState(null);
364
426
  const optionsRef = React.useRef(options);
@@ -377,7 +439,9 @@ function useServiceQuery(serviceName, methodName, payload, options) {
377
439
  timeout = 1e4,
378
440
  retry = 1,
379
441
  retryDelay,
380
- invalidateOn
442
+ invalidateOn,
443
+ refetchInterval,
444
+ refetchIntervalInBackground
381
445
  } = options ?? {};
382
446
  const queryKey = React.useMemo(
383
447
  () => [serviceName, methodName, JSON.stringify(payload)],
@@ -397,11 +461,14 @@ function useServiceQuery(serviceName, methodName, payload, options) {
397
461
  if (response.success) {
398
462
  resolve(response.data);
399
463
  } else {
400
- reject(new Error(response.error));
464
+ if (response.code === 429) {
465
+ reportRateLimited();
466
+ }
467
+ reject(new ServiceCallError(response.error, response.code));
401
468
  }
402
469
  });
403
470
  });
404
- }, [socket, isConnected, serviceName, methodName, payload, timeout]);
471
+ }, [socket, isConnected, serviceName, methodName, payload, timeout, reportRateLimited]);
405
472
  React.useEffect(() => {
406
473
  if (skipCache && enabled) {
407
474
  queryClient.invalidateQueries({ queryKey });
@@ -438,13 +505,17 @@ function useServiceQuery(serviceName, methodName, payload, options) {
438
505
  const query = useQuery({
439
506
  queryKey,
440
507
  queryFn,
441
- enabled: enabled && !!socket && isConnected,
508
+ // Reads pause while the server reports us rate-limited; they resume
509
+ // (and refetch as needed) when the backoff window elapses.
510
+ enabled: enabled && !!socket && isConnected && !isRateLimited,
442
511
  staleTime,
443
512
  gcTime,
444
513
  refetchOnMount,
445
514
  refetchOnWindowFocus,
446
515
  retry,
447
- retryDelay
516
+ retryDelay,
517
+ refetchInterval,
518
+ refetchIntervalInBackground
448
519
  });
449
520
  React.useEffect(() => {
450
521
  if (query.isSuccess && query.data !== void 0) {
@@ -561,6 +632,7 @@ function useSubscription(serviceName, entryId, options = {}) {
561
632
  }
562
633
  };
563
634
  subscriptionBatcher.enqueue(serviceName, entryId, requiredLevel, handleBatchResponse);
635
+ const listenForVisibility = refetchOnWindowFocus && typeof document !== "undefined";
564
636
  let visibilityDebounceTimer = null;
565
637
  const handleVisibilityChange = () => {
566
638
  if (document.visibilityState !== "visible") return;
@@ -570,14 +642,14 @@ function useSubscription(serviceName, entryId, options = {}) {
570
642
  subscriptionBatcher.enqueue(serviceName, entryId, requiredLevel, handleBatchResponse);
571
643
  }, 200);
572
644
  };
573
- if (refetchOnWindowFocus) {
645
+ if (listenForVisibility) {
574
646
  document.addEventListener("visibilitychange", handleVisibilityChange);
575
647
  }
576
648
  const cleanup = () => {
577
649
  socket.off(updateEvent, handleUpdate);
578
650
  socket.emit(`${serviceName}:unsubscribe`, { entryId });
579
651
  if (visibilityDebounceTimer) clearTimeout(visibilityDebounceTimer);
580
- if (refetchOnWindowFocus) {
652
+ if (listenForVisibility) {
581
653
  document.removeEventListener("visibilitychange", handleVisibilityChange);
582
654
  }
583
655
  setIsSubscribed(false);
@@ -633,6 +705,406 @@ function useSubscription(serviceName, entryId, options = {}) {
633
705
  unsubscribe
634
706
  };
635
707
  }
708
+
709
+ // src/shared/types.ts
710
+ function channelEventName(serviceName, channelName) {
711
+ return `channel:${serviceName}:${channelName}`;
712
+ }
713
+ function collectionRoom(serviceName, collection, scopeId) {
714
+ return `${serviceName}:collection:${collection}:${scopeId}`;
715
+ }
716
+
717
+ // src/client/collectionCache.ts
718
+ function createEmptyEntry() {
719
+ return {
720
+ byId: {},
721
+ order: [],
722
+ revById: {},
723
+ nextCursor: null,
724
+ totalCount: null,
725
+ snapshotRev: 0
726
+ };
727
+ }
728
+ function applySnapshot(prev, snapshot) {
729
+ const base = prev ?? createEmptyEntry();
730
+ const pageIds = new Set(snapshot.items.map((item) => item.id));
731
+ const memberIds = snapshot.ids ? new Set(snapshot.ids) : null;
732
+ const byId = {};
733
+ const revById = {};
734
+ for (const id of base.order) {
735
+ const item = base.byId[id];
736
+ if (!item) continue;
737
+ const rev = base.revById[id] ?? 0;
738
+ const liveWins = rev > snapshot.rev;
739
+ const pruned = memberIds !== null && !memberIds.has(id) && !pageIds.has(id) && !liveWins;
740
+ if (pruned) continue;
741
+ byId[id] = item;
742
+ revById[id] = rev;
743
+ }
744
+ for (const item of snapshot.items) {
745
+ const cachedRev = revById[item.id];
746
+ if (cachedRev !== void 0 && cachedRev > snapshot.rev) continue;
747
+ const tombstoneRev = base.revById[item.id];
748
+ if (base.byId[item.id] === void 0 && tombstoneRev !== void 0 && tombstoneRev > snapshot.rev) {
749
+ continue;
750
+ }
751
+ byId[item.id] = item;
752
+ revById[item.id] = snapshot.rev;
753
+ }
754
+ for (const [id, rev] of Object.entries(base.revById)) {
755
+ if (byId[id] === void 0 && rev > snapshot.rev) {
756
+ revById[id] = rev;
757
+ }
758
+ }
759
+ const order = [];
760
+ const seen = /* @__PURE__ */ new Set();
761
+ for (const item of snapshot.items) {
762
+ if (byId[item.id] !== void 0 && !seen.has(item.id)) {
763
+ order.push(item.id);
764
+ seen.add(item.id);
765
+ }
766
+ }
767
+ for (const id of base.order) {
768
+ if (byId[id] !== void 0 && !seen.has(id)) {
769
+ order.push(id);
770
+ seen.add(id);
771
+ }
772
+ }
773
+ return {
774
+ byId,
775
+ order,
776
+ revById,
777
+ nextCursor: snapshot.nextCursor,
778
+ totalCount: snapshot.totalCount,
779
+ snapshotRev: snapshot.rev
780
+ };
781
+ }
782
+ function applyPage(prev, page) {
783
+ const base = prev ?? createEmptyEntry();
784
+ const byId = { ...base.byId };
785
+ const revById = { ...base.revById };
786
+ const order = [...base.order];
787
+ const known = new Set(order);
788
+ for (const item of page.items) {
789
+ const cachedRev = revById[item.id];
790
+ if (cachedRev !== void 0 && cachedRev > page.rev) {
791
+ continue;
792
+ }
793
+ byId[item.id] = item;
794
+ revById[item.id] = page.rev;
795
+ if (!known.has(item.id)) {
796
+ order.push(item.id);
797
+ known.add(item.id);
798
+ }
799
+ }
800
+ return {
801
+ byId,
802
+ order,
803
+ revById,
804
+ nextCursor: page.nextCursor,
805
+ totalCount: page.totalCount,
806
+ snapshotRev: base.snapshotRev
807
+ };
808
+ }
809
+ function applyDelta(prev, delta, options) {
810
+ const base = prev ?? createEmptyEntry();
811
+ if (delta.type === "reset") {
812
+ return { entry: base, resetRequested: true };
813
+ }
814
+ if (delta.type === "removed") {
815
+ const cachedRev2 = base.revById[delta.id];
816
+ if (cachedRev2 !== void 0 && delta.rev < cachedRev2) {
817
+ return { entry: base, resetRequested: false };
818
+ }
819
+ const existed = base.byId[delta.id] !== void 0;
820
+ const byId = { ...base.byId };
821
+ delete byId[delta.id];
822
+ return {
823
+ entry: {
824
+ ...base,
825
+ byId,
826
+ order: existed ? base.order.filter((id) => id !== delta.id) : base.order,
827
+ // Tombstone: keep the removal rev so a stale `added` can't resurrect
828
+ revById: { ...base.revById, [delta.id]: delta.rev },
829
+ totalCount: existed && base.totalCount !== null ? Math.max(0, base.totalCount - 1) : base.totalCount
830
+ },
831
+ resetRequested: false
832
+ };
833
+ }
834
+ const { item, rev } = delta;
835
+ const cachedRev = base.revById[item.id];
836
+ if (cachedRev !== void 0 && rev < cachedRev) {
837
+ return { entry: base, resetRequested: false };
838
+ }
839
+ const isNew = base.byId[item.id] === void 0;
840
+ const insertPosition = options?.insertPosition ?? "end";
841
+ return {
842
+ entry: {
843
+ ...base,
844
+ byId: { ...base.byId, [item.id]: item },
845
+ order: isNew ? insertPosition === "start" ? [item.id, ...base.order] : [...base.order, item.id] : base.order,
846
+ revById: { ...base.revById, [item.id]: rev },
847
+ totalCount: isNew && base.totalCount !== null ? base.totalCount + 1 : base.totalCount
848
+ },
849
+ resetRequested: false
850
+ };
851
+ }
852
+ function applyDeltas(prev, deltas, options) {
853
+ let entry = prev ?? createEmptyEntry();
854
+ let resetRequested = false;
855
+ for (const delta of deltas) {
856
+ const result = applyDelta(entry, delta, options);
857
+ entry = result.entry;
858
+ resetRequested = resetRequested || result.resetRequested;
859
+ }
860
+ return { entry, resetRequested };
861
+ }
862
+
863
+ // src/client/useCollection.ts
864
+ var RESET_DEBOUNCE_MS = 100;
865
+ var REQUEST_TIMEOUT_MS = 1e4;
866
+ var EMPTY_STATE = { entry: null, error: null };
867
+ function useCollection(serviceName, collection, scopeId, options) {
868
+ const { socket, isConnected, subscriptionRegistry } = useQuickdrawSocket();
869
+ const queryClient = useQueryClient();
870
+ const [isLoadingMore, setIsLoadingMore] = React.useState(false);
871
+ const {
872
+ enabled = true,
873
+ limit,
874
+ compare,
875
+ insertPosition = "end",
876
+ onDelta,
877
+ onError
878
+ } = options ?? {};
879
+ const onDeltaRef = React.useRef(onDelta);
880
+ const onErrorRef = React.useRef(onError);
881
+ const insertPositionRef = React.useRef(insertPosition);
882
+ React.useEffect(() => {
883
+ onDeltaRef.current = onDelta;
884
+ onErrorRef.current = onError;
885
+ insertPositionRef.current = insertPosition;
886
+ }, [onDelta, onError, insertPosition]);
887
+ const subscriptionKey = scopeId ? collectionRoom(serviceName, collection, scopeId) : null;
888
+ const queryKey = React.useMemo(
889
+ () => [serviceName, "collection", collection, scopeId],
890
+ [serviceName, collection, scopeId]
891
+ );
892
+ const query = useQuery({
893
+ queryKey,
894
+ queryFn: () => queryClient.getQueryData(queryKey) ?? EMPTY_STATE,
895
+ initialData: () => EMPTY_STATE,
896
+ staleTime: Infinity,
897
+ refetchOnMount: false,
898
+ refetchOnWindowFocus: false,
899
+ refetchOnReconnect: false
900
+ });
901
+ const regEntryRef = React.useRef(null);
902
+ React.useEffect(() => {
903
+ if (!socket || !isConnected || !enabled || !subscriptionKey || !scopeId) {
904
+ return;
905
+ }
906
+ const { isNew, entry: regEntry } = subscriptionRegistry.acquire(subscriptionKey);
907
+ regEntryRef.current = regEntry;
908
+ if (!isNew) {
909
+ return () => {
910
+ regEntryRef.current = null;
911
+ subscriptionRegistry.release(subscriptionKey);
912
+ };
913
+ }
914
+ const eventName = subscriptionKey;
915
+ let disposed = false;
916
+ let snapshotInFlight = false;
917
+ let buffered = [];
918
+ let inFlightSnapshot = null;
919
+ let inFlightLoadMore = null;
920
+ let resetTimer = null;
921
+ const readState = () => queryClient.getQueryData(queryKey) ?? EMPTY_STATE;
922
+ const writeState = (state2) => {
923
+ queryClient.setQueryData(queryKey, state2);
924
+ };
925
+ const emitSubscribe = (cursor) => new Promise((resolve) => {
926
+ let settled = false;
927
+ const timeoutId = setTimeout(() => {
928
+ if (!settled) {
929
+ settled = true;
930
+ resolve({ success: false, error: "Request timeout" });
931
+ }
932
+ }, REQUEST_TIMEOUT_MS);
933
+ socket.emit(
934
+ `${serviceName}:collection:subscribe`,
935
+ {
936
+ collection,
937
+ scopeId,
938
+ ...cursor === null ? {} : { cursor },
939
+ ...limit === void 0 ? {} : { limit }
940
+ },
941
+ (response) => {
942
+ if (settled) return;
943
+ settled = true;
944
+ clearTimeout(timeoutId);
945
+ resolve(response);
946
+ }
947
+ );
948
+ });
949
+ const scheduleReset = () => {
950
+ if (resetTimer || disposed) return;
951
+ resetTimer = setTimeout(() => {
952
+ resetTimer = null;
953
+ void requestSnapshot();
954
+ }, RESET_DEBOUNCE_MS);
955
+ };
956
+ const requestSnapshot = () => {
957
+ if (inFlightSnapshot) return inFlightSnapshot;
958
+ snapshotInFlight = true;
959
+ buffered = [];
960
+ inFlightSnapshot = emitSubscribe(null).then((response) => {
961
+ snapshotInFlight = false;
962
+ inFlightSnapshot = null;
963
+ const raced = buffered;
964
+ buffered = [];
965
+ if (disposed) return;
966
+ if (response.success) {
967
+ const prev = readState();
968
+ const fresh = applySnapshot(prev.entry, response.data);
969
+ const { entry: entry2, resetRequested } = applyDeltas(fresh, raced, {
970
+ insertPosition: insertPositionRef.current
971
+ });
972
+ writeState({ entry: entry2, error: null });
973
+ if (resetRequested) scheduleReset();
974
+ } else {
975
+ const prev = readState();
976
+ writeState({ entry: prev.entry, error: response.error });
977
+ }
978
+ });
979
+ return inFlightSnapshot;
980
+ };
981
+ const loadMore2 = () => {
982
+ if (inFlightLoadMore) return inFlightLoadMore;
983
+ const cursor = readState().entry?.nextCursor ?? null;
984
+ if (cursor === null) return Promise.resolve();
985
+ inFlightLoadMore = emitSubscribe(cursor).then((response) => {
986
+ inFlightLoadMore = null;
987
+ if (disposed) return;
988
+ const prev = readState();
989
+ if (response.success) {
990
+ writeState({ entry: applyPage(prev.entry, response.data), error: prev.error });
991
+ } else {
992
+ writeState({ entry: prev.entry, error: response.error });
993
+ }
994
+ });
995
+ return inFlightLoadMore;
996
+ };
997
+ const handleDelta = (delta) => {
998
+ if (delta.type === "reset") {
999
+ scheduleReset();
1000
+ return;
1001
+ }
1002
+ if (snapshotInFlight) {
1003
+ buffered.push(delta);
1004
+ return;
1005
+ }
1006
+ const prev = readState();
1007
+ const { entry: entry2 } = applyDelta(prev.entry, delta, {
1008
+ insertPosition: insertPositionRef.current
1009
+ });
1010
+ if (entry2 !== prev.entry) {
1011
+ writeState({ entry: entry2, error: prev.error });
1012
+ }
1013
+ };
1014
+ const controller = { requestSnapshot, loadMore: loadMore2 };
1015
+ regEntry.controller = controller;
1016
+ socket.on(eventName, handleDelta);
1017
+ void requestSnapshot();
1018
+ subscriptionRegistry.setCleanup(subscriptionKey, () => {
1019
+ disposed = true;
1020
+ if (resetTimer) clearTimeout(resetTimer);
1021
+ socket.off(eventName, handleDelta);
1022
+ socket.emit(`${serviceName}:collection:unsubscribe`, { collection, scopeId });
1023
+ regEntry.controller = void 0;
1024
+ });
1025
+ return () => {
1026
+ regEntryRef.current = null;
1027
+ subscriptionRegistry.release(subscriptionKey);
1028
+ };
1029
+ }, [
1030
+ socket,
1031
+ isConnected,
1032
+ enabled,
1033
+ subscriptionKey,
1034
+ scopeId,
1035
+ serviceName,
1036
+ collection,
1037
+ limit,
1038
+ queryClient,
1039
+ queryKey,
1040
+ subscriptionRegistry
1041
+ ]);
1042
+ const wantsDelta = !!onDelta;
1043
+ React.useEffect(() => {
1044
+ if (!socket || !isConnected || !enabled || !subscriptionKey || !wantsDelta) {
1045
+ return;
1046
+ }
1047
+ const listener = (delta) => {
1048
+ onDeltaRef.current?.(delta);
1049
+ };
1050
+ socket.on(subscriptionKey, listener);
1051
+ return () => {
1052
+ socket.off(subscriptionKey, listener);
1053
+ };
1054
+ }, [socket, isConnected, enabled, subscriptionKey, wantsDelta]);
1055
+ const state = query.data ?? EMPTY_STATE;
1056
+ const entry = state.entry;
1057
+ React.useEffect(() => {
1058
+ if (state.error) {
1059
+ onErrorRef.current?.(state.error);
1060
+ }
1061
+ }, [state.error]);
1062
+ const getController = () => regEntryRef.current?.controller ?? null;
1063
+ const getControllerRef = React.useRef(getController);
1064
+ getControllerRef.current = getController;
1065
+ const refresh = React.useCallback(async () => {
1066
+ await getControllerRef.current()?.requestSnapshot();
1067
+ }, []);
1068
+ const loadMore = React.useCallback(async () => {
1069
+ const controller = getControllerRef.current();
1070
+ if (!controller) return;
1071
+ setIsLoadingMore(true);
1072
+ try {
1073
+ await controller.loadMore();
1074
+ } finally {
1075
+ setIsLoadingMore(false);
1076
+ }
1077
+ }, []);
1078
+ const items = React.useMemo(() => {
1079
+ if (!entry) return [];
1080
+ const list = entry.order.map((id) => entry.byId[id]).filter((item) => item !== void 0);
1081
+ if (compare) list.sort(compare);
1082
+ return list;
1083
+ }, [entry, compare]);
1084
+ const byId = React.useMemo(() => {
1085
+ const map = /* @__PURE__ */ new Map();
1086
+ if (entry) {
1087
+ for (const id of entry.order) {
1088
+ const item = entry.byId[id];
1089
+ if (item !== void 0) map.set(id, item);
1090
+ }
1091
+ }
1092
+ return map;
1093
+ }, [entry]);
1094
+ const active = enabled && !!scopeId;
1095
+ return {
1096
+ items,
1097
+ byId,
1098
+ totalCount: entry?.totalCount ?? null,
1099
+ isLoading: active && !entry && !state.error,
1100
+ isError: state.error !== null,
1101
+ error: state.error,
1102
+ hasMore: (entry?.nextCursor ?? null) !== null,
1103
+ isLoadingMore,
1104
+ loadMore,
1105
+ refresh
1106
+ };
1107
+ }
636
1108
  function useRoomEvents(handlers, options) {
637
1109
  const { socket, isConnected } = useQuickdrawSocket();
638
1110
  const { enabled = true } = options ?? {};
@@ -640,7 +1112,7 @@ function useRoomEvents(handlers, options) {
640
1112
  React.useEffect(() => {
641
1113
  handlersRef.current = handlers;
642
1114
  }, [handlers]);
643
- const eventNames = Object.keys(handlers);
1115
+ const eventNames = Object.keys(handlers).filter((name) => handlers[name] != null);
644
1116
  const eventNamesKey = eventNames.join("\0");
645
1117
  const stableEventNames = React.useMemo(
646
1118
  () => eventNames,
@@ -666,13 +1138,6 @@ function useRoomEvents(handlers, options) {
666
1138
  };
667
1139
  }, [socket, isConnected, enabled, stableEventNames]);
668
1140
  }
669
-
670
- // src/shared/types.ts
671
- function channelEventName(serviceName, channelName) {
672
- return `channel:${serviceName}:${channelName}`;
673
- }
674
-
675
- // src/client/useChannelSend.ts
676
1141
  function useChannelSend(serviceName, channelName) {
677
1142
  const { socket, isConnected } = useQuickdrawSocket();
678
1143
  const eventName = channelEventName(serviceName, channelName);
@@ -835,6 +1300,8 @@ function SocketTextField(props) {
835
1300
  onSuccess,
836
1301
  onError,
837
1302
  disabled,
1303
+ type = "text",
1304
+ onBlur: consumerOnBlur,
838
1305
  ...inputProps
839
1306
  } = props;
840
1307
  const defaultFormat = React.useCallback((v) => String(v ?? ""), []);
@@ -856,14 +1323,21 @@ function SocketTextField(props) {
856
1323
  },
857
1324
  [onLocalChange]
858
1325
  );
1326
+ const handleBlur = React.useCallback(
1327
+ (event) => {
1328
+ onBlur();
1329
+ consumerOnBlur?.(event);
1330
+ },
1331
+ [onBlur, consumerOnBlur]
1332
+ );
859
1333
  return /* @__PURE__ */ jsx(
860
1334
  "input",
861
1335
  {
862
1336
  ...inputProps,
863
- type: "text",
1337
+ type,
864
1338
  value: String(value),
865
1339
  onChange: handleChange,
866
- onBlur,
1340
+ onBlur: handleBlur,
867
1341
  disabled: Boolean(disabled) || inFlight
868
1342
  }
869
1343
  );
@@ -1215,6 +1689,6 @@ function routeRequiresAuth(pathname, navigation, userMenuItems) {
1215
1689
  return false;
1216
1690
  }
1217
1691
 
1218
- export { QuickdrawProvider, SocketCheckbox, SocketSelect, SocketSlider, SocketSwitch, SocketTextField, buildBreadcrumbs, clearAuthToken, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useChannelSend, useQuickdrawSocket, useRoomEvents, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
1692
+ export { QuickdrawProvider, ServiceCallError, SocketCheckbox, SocketSelect, SocketSlider, SocketSwitch, SocketTextField, applyDelta, applyDeltas, applyPage, applySnapshot, buildBreadcrumbs, clearAuthToken, createEmptyEntry, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useChannelSend, useCollection, useQuickdrawSocket, useRoomEvents, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
1219
1693
  //# sourceMappingURL=index.js.map
1220
1694
  //# sourceMappingURL=index.js.map