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