sonamu 0.9.4 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/providers/rtzr/utils.js +2 -2
- package/dist/api/config.d.ts +13 -2
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/context.d.ts +17 -7
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/context.js +1 -1
- package/dist/api/decorators.d.ts +18 -0
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +54 -3
- package/dist/api/index.js +8 -3
- package/dist/api/sonamu.d.ts +24 -9
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +365 -79
- package/dist/api/websocket-helpers.d.ts +24 -0
- package/dist/api/websocket-helpers.d.ts.map +1 -0
- package/dist/api/websocket-helpers.js +77 -0
- package/dist/bin/cli.js +12 -4
- package/dist/database/upsert-builder.js +4 -4
- package/dist/dict/sonamu-dictionary.js +6 -6
- package/dist/entity/entity-manager.js +1 -1
- package/dist/entity/entity.js +3 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +8 -9
- package/dist/stream/index.d.ts +6 -0
- package/dist/stream/index.d.ts.map +1 -1
- package/dist/stream/index.js +13 -2
- package/dist/stream/ws-audience-resolver.d.ts +15 -0
- package/dist/stream/ws-audience-resolver.d.ts.map +1 -0
- package/dist/stream/ws-audience-resolver.js +31 -0
- package/dist/stream/ws-audience.d.ts +28 -0
- package/dist/stream/ws-audience.d.ts.map +1 -0
- package/dist/stream/ws-audience.js +46 -0
- package/dist/stream/ws-cluster-bus.d.ts +23 -0
- package/dist/stream/ws-cluster-bus.d.ts.map +1 -0
- package/dist/stream/ws-cluster-bus.js +18 -0
- package/dist/stream/ws-core.d.ts +15 -0
- package/dist/stream/ws-core.d.ts.map +1 -0
- package/dist/stream/ws-core.js +1 -0
- package/dist/stream/ws-delivery.d.ts +24 -0
- package/dist/stream/ws-delivery.d.ts.map +1 -0
- package/dist/stream/ws-delivery.js +103 -0
- package/dist/stream/ws-local-connection-store.d.ts +10 -0
- package/dist/stream/ws-local-connection-store.d.ts.map +1 -0
- package/dist/stream/ws-local-connection-store.js +44 -0
- package/dist/stream/ws-presence-store.d.ts +61 -0
- package/dist/stream/ws-presence-store.d.ts.map +1 -0
- package/dist/stream/ws-presence-store.js +236 -0
- package/dist/stream/ws-registry.d.ts +42 -0
- package/dist/stream/ws-registry.d.ts.map +1 -0
- package/dist/stream/ws-registry.js +108 -0
- package/dist/stream/ws.d.ts +52 -0
- package/dist/stream/ws.d.ts.map +1 -0
- package/dist/stream/ws.js +397 -0
- package/dist/syncer/api-parser.d.ts.map +1 -1
- package/dist/syncer/api-parser.js +72 -2
- package/dist/syncer/checksum.d.ts.map +1 -1
- package/dist/syncer/checksum.js +13 -12
- package/dist/syncer/code-generator.d.ts.map +1 -1
- package/dist/syncer/code-generator.js +7 -4
- package/dist/syncer/event-batcher.d.ts +27 -0
- package/dist/syncer/event-batcher.d.ts.map +1 -0
- package/dist/syncer/event-batcher.js +69 -0
- package/dist/syncer/file-patterns.d.ts +48 -26
- package/dist/syncer/file-patterns.d.ts.map +1 -1
- package/dist/syncer/file-patterns.js +71 -23
- package/dist/syncer/file-tracking.d.ts +13 -0
- package/dist/syncer/file-tracking.d.ts.map +1 -0
- package/dist/syncer/file-tracking.js +33 -0
- package/dist/syncer/index.js +2 -2
- package/dist/syncer/module-loader.d.ts +2 -11
- package/dist/syncer/module-loader.d.ts.map +1 -1
- package/dist/syncer/module-loader.js +3 -3
- package/dist/syncer/syncer-actions.d.ts +39 -6
- package/dist/syncer/syncer-actions.d.ts.map +1 -1
- package/dist/syncer/syncer-actions.js +125 -10
- package/dist/syncer/syncer.d.ts +33 -19
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +168 -168
- package/dist/syncer/watcher.d.ts +8 -0
- package/dist/syncer/watcher.d.ts.map +1 -0
- package/dist/syncer/watcher.js +105 -0
- package/dist/tasks/workflow-manager.d.ts.map +1 -1
- package/dist/tasks/workflow-manager.js +2 -1
- package/dist/template/implementations/services.template.d.ts.map +1 -1
- package/dist/template/implementations/services.template.js +36 -1
- package/dist/testing/bootstrap.d.ts.map +1 -1
- package/dist/testing/bootstrap.js +8 -1
- package/dist/testing/data-explorer.d.ts.map +1 -1
- package/dist/testing/data-explorer.js +5 -3
- package/dist/testing/fixture-manager.js +1 -1
- package/dist/types/types.d.ts +2 -1
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +2 -2
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +4 -3
- package/dist/ui/cdd-service.js +1 -1
- package/dist/ui-web/assets/{index-C5KUjXm0.js → index-BmThfg-s.js} +39 -39
- package/dist/ui-web/assets/index-D4rYm-Xz.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/dist/utils/async-utils.d.ts +27 -3
- package/dist/utils/async-utils.d.ts.map +1 -1
- package/dist/utils/async-utils.js +56 -6
- package/dist/utils/formatter.d.ts +7 -1
- package/dist/utils/formatter.d.ts.map +1 -1
- package/dist/utils/formatter.js +95 -60
- package/dist/utils/fs-utils.d.ts +2 -0
- package/dist/utils/fs-utils.d.ts.map +1 -1
- package/dist/utils/fs-utils.js +10 -2
- package/dist/utils/process-utils.d.ts +6 -0
- package/dist/utils/process-utils.d.ts.map +1 -1
- package/dist/utils/process-utils.js +16 -3
- package/dist/utils/utils.d.ts +1 -0
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +2 -2
- package/package.json +7 -5
- package/src/ai/providers/rtzr/utils.ts +1 -1
- package/src/api/__tests__/sonamu.websocket.test.ts +64 -0
- package/src/api/__tests__/websocket-context.types.test.ts +58 -0
- package/src/api/config.ts +28 -2
- package/src/api/context.ts +21 -7
- package/src/api/decorators.ts +101 -1
- package/src/api/sonamu.ts +529 -127
- package/src/api/websocket-helpers.ts +122 -0
- package/src/bin/cli.ts +10 -2
- package/src/database/upsert-builder.ts +3 -3
- package/src/dict/sonamu-dictionary.ts +3 -3
- package/src/entity/entity.ts +1 -1
- package/src/index.ts +6 -0
- package/src/migration/code-generation.ts +6 -11
- package/src/shared/app.shared.ts.txt +312 -4
- package/src/shared/web.shared.ts.txt +340 -4
- package/src/stream/__tests__/ws-contracts.test.ts +381 -0
- package/src/stream/__tests__/ws.test.ts +449 -0
- package/src/stream/index.ts +6 -0
- package/src/stream/ws-audience-resolver.ts +35 -0
- package/src/stream/ws-audience.ts +62 -0
- package/src/stream/ws-cluster-bus.ts +32 -0
- package/src/stream/ws-core.ts +16 -0
- package/src/stream/ws-delivery.ts +138 -0
- package/src/stream/ws-local-connection-store.ts +44 -0
- package/src/stream/ws-presence-store.ts +326 -0
- package/src/stream/ws-registry.ts +138 -0
- package/src/stream/ws.ts +591 -0
- package/src/syncer/__tests__/api-parser.websocket-type-ref.test.ts +78 -0
- package/src/syncer/api-parser.ts +112 -1
- package/src/syncer/checksum.ts +23 -29
- package/src/syncer/code-generator.ts +4 -1
- package/src/syncer/event-batcher.ts +72 -0
- package/src/syncer/file-patterns.ts +98 -30
- package/src/syncer/file-tracking.ts +27 -0
- package/src/syncer/module-loader.ts +5 -12
- package/src/syncer/syncer-actions.ts +179 -17
- package/src/syncer/syncer.ts +250 -287
- package/src/syncer/watcher.ts +128 -0
- package/src/tasks/workflow-manager.ts +1 -0
- package/src/template/__tests__/services.template.websocket.test.ts +79 -0
- package/src/template/implementations/services.template.ts +69 -0
- package/src/testing/bootstrap.ts +8 -1
- package/src/testing/data-explorer.ts +3 -2
- package/src/types/types.ts +20 -2
- package/src/ui/api.ts +10 -1
- package/src/utils/async-utils.ts +71 -4
- package/src/utils/formatter.ts +114 -75
- package/src/utils/fs-utils.ts +9 -0
- package/src/utils/process-utils.ts +17 -0
- package/src/utils/utils.ts +1 -1
- package/dist/ui-web/assets/index-Dr8pRJC_.css +0 -1
|
@@ -13,8 +13,9 @@ import type { AxiosRequestConfig } from "axios";
|
|
|
13
13
|
import axios from "axios";
|
|
14
14
|
import { EventSource } from "eventsource";
|
|
15
15
|
import qs from "qs";
|
|
16
|
-
import { useEffect, useRef, useState } from "react";
|
|
16
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
17
17
|
import { type core, z } from "zod";
|
|
18
|
+
import { type InfiniteData } from "@tanstack/react-query";
|
|
18
19
|
import { getCurrentLocale } from "@/i18n/sd.generated";
|
|
19
20
|
|
|
20
21
|
// ISO 8601 및 타임존 포맷의 날짜 문자열을 Date 객체로 변환하는 reviver
|
|
@@ -325,8 +326,23 @@ export type SSEStreamState = {
|
|
|
325
326
|
retryCount: number;
|
|
326
327
|
isEnded: boolean;
|
|
327
328
|
};
|
|
329
|
+
export type WebSocketChannelOptions = {
|
|
330
|
+
enabled?: boolean;
|
|
331
|
+
retry?: number;
|
|
332
|
+
retryInterval?: number;
|
|
333
|
+
protocols?: string | string[];
|
|
334
|
+
};
|
|
335
|
+
export type WebSocketChannelState<TSend extends Record<string, any>> = {
|
|
336
|
+
isConnected: boolean;
|
|
337
|
+
error: string | null;
|
|
338
|
+
retryCount: number;
|
|
339
|
+
readyState: number;
|
|
340
|
+
send<K extends keyof TSend>(event: K, data: TSend[K]): void;
|
|
341
|
+
close(code?: number, reason?: string): void;
|
|
342
|
+
};
|
|
343
|
+
// outbound event 전체를 강제하지 않도록 handler를 optional map으로 둠
|
|
328
344
|
export type EventHandlers<T> = {
|
|
329
|
-
[K in keyof T]
|
|
345
|
+
[K in keyof T]?: (data: T[K]) => void;
|
|
330
346
|
};
|
|
331
347
|
|
|
332
348
|
export function useSSEStream<T extends Record<string, any>>(
|
|
@@ -480,7 +496,7 @@ export function useSSEStream<T extends Record<string, any>>(
|
|
|
480
496
|
}
|
|
481
497
|
|
|
482
498
|
try {
|
|
483
|
-
const data = JSON.parse(event.data);
|
|
499
|
+
const data = JSON.parse(event.data, dateReviver);
|
|
484
500
|
handler(data);
|
|
485
501
|
} catch (error) {
|
|
486
502
|
console.error(`Failed to parse SSE data for event ${eventType}:`, error);
|
|
@@ -501,7 +517,7 @@ export function useSSEStream<T extends Record<string, any>>(
|
|
|
501
517
|
}
|
|
502
518
|
|
|
503
519
|
try {
|
|
504
|
-
const data = JSON.parse(event.data);
|
|
520
|
+
const data = JSON.parse(event.data, dateReviver);
|
|
505
521
|
// 'message' 핸들러가 있으면 호출
|
|
506
522
|
const messageHandler = handlersRef.current["message" as keyof T];
|
|
507
523
|
if (messageHandler) {
|
|
@@ -550,7 +566,327 @@ export function useSSEStream<T extends Record<string, any>>(
|
|
|
550
566
|
return state;
|
|
551
567
|
}
|
|
552
568
|
|
|
569
|
+
export function useWebSocketChannel<
|
|
570
|
+
TReceive extends Record<string, any>,
|
|
571
|
+
TSend extends Record<string, any>,
|
|
572
|
+
>(
|
|
573
|
+
url: string,
|
|
574
|
+
params: Record<string, any>,
|
|
575
|
+
handlers: EventHandlers<TReceive>,
|
|
576
|
+
options: WebSocketChannelOptions = {},
|
|
577
|
+
): WebSocketChannelState<TSend> {
|
|
578
|
+
const { enabled = true, retry = 3, retryInterval = 3000, protocols } = options;
|
|
579
|
+
|
|
580
|
+
const [state, setState] = useState<Omit<WebSocketChannelState<TSend>, "send" | "close">>({
|
|
581
|
+
isConnected: false,
|
|
582
|
+
error: null,
|
|
583
|
+
retryCount: 0,
|
|
584
|
+
readyState: 3,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const socketRef = useRef<WebSocket | null>(null);
|
|
588
|
+
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
589
|
+
const handlersRef = useRef(handlers);
|
|
590
|
+
const manualCloseRef = useRef(false);
|
|
591
|
+
// 최신 연결 식별자를 따로 두어 stale socket 이벤트가 상태를 덮지 못하게 함
|
|
592
|
+
const connectionIdRef = useRef(0);
|
|
593
|
+
|
|
594
|
+
useEffect(() => {
|
|
595
|
+
handlersRef.current = handlers;
|
|
596
|
+
}, [handlers]);
|
|
597
|
+
|
|
598
|
+
const close = (code?: number, reason?: string) => {
|
|
599
|
+
manualCloseRef.current = true;
|
|
600
|
+
if (retryTimeoutRef.current) {
|
|
601
|
+
clearTimeout(retryTimeoutRef.current);
|
|
602
|
+
retryTimeoutRef.current = null;
|
|
603
|
+
}
|
|
604
|
+
if (socketRef.current) {
|
|
605
|
+
socketRef.current.close(code, reason);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const send = <K extends keyof TSend>(event: K, data: TSend[K]) => {
|
|
610
|
+
const socket = socketRef.current;
|
|
611
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
612
|
+
setState((prev) => ({
|
|
613
|
+
...prev,
|
|
614
|
+
error: "WebSocket is not connected",
|
|
615
|
+
}));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
socket.send(
|
|
620
|
+
JSON.stringify({
|
|
621
|
+
event,
|
|
622
|
+
data,
|
|
623
|
+
}),
|
|
624
|
+
);
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const connect = () => {
|
|
628
|
+
if (!enabled) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const connectionId = ++connectionIdRef.current;
|
|
633
|
+
manualCloseRef.current = false;
|
|
634
|
+
|
|
635
|
+
if (socketRef.current) {
|
|
636
|
+
socketRef.current.close();
|
|
637
|
+
socketRef.current = null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (retryTimeoutRef.current) {
|
|
641
|
+
clearTimeout(retryTimeoutRef.current);
|
|
642
|
+
retryTimeoutRef.current = null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const fullUrl = resolveWebSocketUrl(url, params);
|
|
646
|
+
const socket = new WebSocket(fullUrl, protocols);
|
|
647
|
+
socketRef.current = socket;
|
|
648
|
+
|
|
649
|
+
setState((prev) => ({
|
|
650
|
+
...prev,
|
|
651
|
+
isConnected: false,
|
|
652
|
+
error: null,
|
|
653
|
+
readyState: socket.readyState,
|
|
654
|
+
}));
|
|
655
|
+
|
|
656
|
+
// socketRef.current !== socket 가드는 이전 연결의 늦은 콜백을 무시하기 위한 장치임
|
|
657
|
+
socket.addEventListener("open", () => {
|
|
658
|
+
if (socketRef.current !== socket) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
setState((prev) => ({
|
|
663
|
+
...prev,
|
|
664
|
+
isConnected: true,
|
|
665
|
+
error: null,
|
|
666
|
+
retryCount: 0,
|
|
667
|
+
readyState: socket.readyState,
|
|
668
|
+
}));
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
socket.addEventListener("message", (event) => {
|
|
672
|
+
if (socketRef.current !== socket) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
const payload = JSON.parse(event.data, dateReviver) as {
|
|
678
|
+
event: keyof TReceive;
|
|
679
|
+
data: TReceive[keyof TReceive];
|
|
680
|
+
};
|
|
681
|
+
const handler = handlersRef.current[payload.event];
|
|
682
|
+
if (handler) {
|
|
683
|
+
handler(payload.data);
|
|
684
|
+
}
|
|
685
|
+
} catch (error) {
|
|
686
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
socket.addEventListener("error", () => {
|
|
691
|
+
if (socketRef.current !== socket) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
setState((prev) => ({
|
|
696
|
+
...prev,
|
|
697
|
+
isConnected: false,
|
|
698
|
+
error: "WebSocket connection failed",
|
|
699
|
+
readyState: socket.readyState,
|
|
700
|
+
}));
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
socket.addEventListener("close", (event) => {
|
|
704
|
+
if (socketRef.current !== socket) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
socketRef.current = null;
|
|
709
|
+
|
|
710
|
+
setState((prev) => ({
|
|
711
|
+
...prev,
|
|
712
|
+
isConnected: false,
|
|
713
|
+
readyState: socket.readyState,
|
|
714
|
+
}));
|
|
715
|
+
|
|
716
|
+
if (manualCloseRef.current || connectionIdRef.current !== connectionId) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// 정책 위반/과대 payload close는 재시도보다 명시적 에러 노출이 우선임
|
|
721
|
+
if (!isRetryableWebSocketCloseCode(event.code)) {
|
|
722
|
+
setState((prev) => ({
|
|
723
|
+
...prev,
|
|
724
|
+
error:
|
|
725
|
+
event.code === 1008 || event.code === 1009
|
|
726
|
+
? `WebSocket rejected by server (code: ${event.code})`
|
|
727
|
+
: `WebSocket closed (code: ${event.code})`,
|
|
728
|
+
}));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
setState((prev) => {
|
|
733
|
+
if (prev.retryCount >= retry) {
|
|
734
|
+
return {
|
|
735
|
+
...prev,
|
|
736
|
+
error: `Connection failed after ${retry} attempts`,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
retryTimeoutRef.current = setTimeout(() => {
|
|
741
|
+
if (connectionIdRef.current !== connectionId) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
setState((inner) => ({
|
|
746
|
+
...inner,
|
|
747
|
+
retryCount: inner.retryCount + 1,
|
|
748
|
+
}));
|
|
749
|
+
connect();
|
|
750
|
+
}, retryInterval);
|
|
751
|
+
|
|
752
|
+
return prev;
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
useEffect(() => {
|
|
758
|
+
if (enabled) {
|
|
759
|
+
setState({
|
|
760
|
+
isConnected: false,
|
|
761
|
+
error: null,
|
|
762
|
+
retryCount: 0,
|
|
763
|
+
readyState: 3,
|
|
764
|
+
});
|
|
765
|
+
connect();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return () => {
|
|
769
|
+
connectionIdRef.current += 1;
|
|
770
|
+
manualCloseRef.current = true;
|
|
771
|
+
if (socketRef.current) {
|
|
772
|
+
socketRef.current.close();
|
|
773
|
+
socketRef.current = null;
|
|
774
|
+
}
|
|
775
|
+
if (retryTimeoutRef.current) {
|
|
776
|
+
clearTimeout(retryTimeoutRef.current);
|
|
777
|
+
retryTimeoutRef.current = null;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
}, [url, JSON.stringify(params), enabled, JSON.stringify(protocols)]);
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
...state,
|
|
784
|
+
send,
|
|
785
|
+
close,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function isRetryableWebSocketCloseCode(code: number): boolean {
|
|
790
|
+
if (code === 1000) {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return ![1002, 1003, 1007, 1008, 1009].includes(code);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function resolveWebSocketUrl(url: string, params: Record<string, any>): string {
|
|
798
|
+
const queryString = qs.stringify(params);
|
|
799
|
+
const withQuery = queryString ? `${url}?${queryString}` : url;
|
|
800
|
+
|
|
801
|
+
if (withQuery.startsWith("ws://") || withQuery.startsWith("wss://")) {
|
|
802
|
+
return withQuery;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const baseUrl = resolveWebSocketBaseUrl();
|
|
806
|
+
return new URL(withQuery, baseUrl).toString();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function resolveWebSocketBaseUrl(): string {
|
|
810
|
+
const configuredBaseUrl = axios.defaults.baseURL;
|
|
811
|
+
// HTTP client와 WS client가 다른 origin으로 갈라지지 않게 axios baseURL을 우선 존중함
|
|
812
|
+
if (configuredBaseUrl) {
|
|
813
|
+
const absoluteBaseUrl =
|
|
814
|
+
typeof window !== "undefined"
|
|
815
|
+
? new URL(configuredBaseUrl, window.location.origin).toString()
|
|
816
|
+
: configuredBaseUrl;
|
|
817
|
+
return toWebSocketBaseUrl(absoluteBaseUrl);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (typeof window !== "undefined") {
|
|
821
|
+
return toWebSocketBaseUrl(window.location.origin);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return toWebSocketBaseUrl("$[[baseUrl]]");
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function toWebSocketBaseUrl(baseUrl: string): string {
|
|
828
|
+
if (baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://")) {
|
|
829
|
+
return baseUrl;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return baseUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
833
|
+
}
|
|
834
|
+
|
|
553
835
|
/*
|
|
554
836
|
Dictionary Helper
|
|
555
837
|
*/
|
|
556
838
|
$[[dictUtils]]
|
|
839
|
+
/*
|
|
840
|
+
Query helpers
|
|
841
|
+
*/
|
|
842
|
+
type InfinitePage<TRow> = { rows: TRow[]; total: number };
|
|
843
|
+
type DedupedInfiniteData<TRow> = InfiniteData<InfinitePage<TRow>> & {
|
|
844
|
+
rows: TRow[];
|
|
845
|
+
total: number;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// useInfiniteQuery의 select에 꽂아 pages/pageParams 원본은 유지하면서
|
|
849
|
+
// 평탄화된 rows와 첫 페이지의 total을 data에 함께 노출합니다.
|
|
850
|
+
// 각 row가 id를 갖는 경우 id 기준으로 중복 제거합니다. id가 없으면 그대로 유지합니다.
|
|
851
|
+
export function dedupeAndFlatten<TRow extends { id?: unknown }>(
|
|
852
|
+
data: InfiniteData<InfinitePage<TRow>>,
|
|
853
|
+
): DedupedInfiniteData<TRow> {
|
|
854
|
+
const seen = new Set<unknown>();
|
|
855
|
+
const rows: TRow[] = [];
|
|
856
|
+
for (const page of data.pages) {
|
|
857
|
+
for (const row of page?.rows ?? []) {
|
|
858
|
+
const id = row?.id;
|
|
859
|
+
if (id !== null) {
|
|
860
|
+
if (seen.has(id)) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
seen.add(id);
|
|
864
|
+
}
|
|
865
|
+
rows.push(row);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const total = data.pages[0]?.total ?? 0;
|
|
869
|
+
return {
|
|
870
|
+
pages: data.pages,
|
|
871
|
+
pageParams: data.pageParams,
|
|
872
|
+
rows,
|
|
873
|
+
total,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// TanStack Query 결과에 수동 refresh 진입점과 새로고침 중 상태를 덧붙여 줍니다.
|
|
878
|
+
// isRefreshing은 query.isFetching과 독립적으로 이 함수 호출로 발생한 새로고침에 한정됩니다.
|
|
879
|
+
export function useRefreshable<T extends { refetch: () => Promise<unknown> }>(
|
|
880
|
+
query: T,
|
|
881
|
+
): T & { refresh: () => Promise<void>; isRefreshing: boolean } {
|
|
882
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
883
|
+
const refresh = useCallback(async () => {
|
|
884
|
+
setIsRefreshing(true);
|
|
885
|
+
try {
|
|
886
|
+
await query.refetch();
|
|
887
|
+
} finally {
|
|
888
|
+
setIsRefreshing(false);
|
|
889
|
+
}
|
|
890
|
+
}, [query]);
|
|
891
|
+
return { ...query, refresh, isRefreshing };
|
|
892
|
+
}
|