@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.
- package/README.md +201 -25
- package/dist/client/index.d.ts +132 -6
- package/dist/client/index.js +499 -25
- 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-BbnMKDA5.d.ts} +104 -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,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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|