@fitzzero/quickdraw-core 3.9.1 → 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.
- package/README.md +201 -25
- package/dist/client/index.d.ts +132 -6
- package/dist/client/index.js +492 -22
- package/dist/client/index.js.map +1 -1
- package/dist/client/testing.d.ts +1 -1
- package/dist/client/testing.js +3 -0
- package/dist/client/testing.js.map +1 -1
- package/dist/server/index.d.ts +402 -34
- package/dist/server/index.js +610 -89
- package/dist/server/index.js.map +1 -1
- package/dist/server/testing.d.ts +1 -1
- package/dist/server/testing.js +98 -3
- package/dist/server/testing.js.map +1 -1
- package/dist/shared/index.d.ts +154 -31
- package/dist/shared/index.js +16 -1
- package/dist/shared/index.js.map +1 -1
- package/dist/{types-ClgSr90l.d.ts → types-87lWXyNP.d.ts} +36 -32
- package/dist/{types-DUHPIy_5.d.ts → types-CX5LuC_8.d.ts} +94 -2
- package/eslint-plugin-quickdraw/index.mjs +4 -0
- package/eslint-plugin-quickdraw/rules/no-manual-collection-events.mjs +68 -0
- package/oxlint.base.jsonc +3 -0
- package/package.json +1 -1
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|