@simplysm/solid 13.0.33 → 13.0.35

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.
Files changed (79) hide show
  1. package/README.md +22 -42
  2. package/dist/components/disclosure/DialogContext.d.ts +29 -0
  3. package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
  4. package/dist/components/disclosure/DialogContext.js.map +1 -1
  5. package/dist/components/disclosure/DialogInstanceContext.d.ts +14 -0
  6. package/dist/components/disclosure/DialogInstanceContext.d.ts.map +1 -1
  7. package/dist/components/disclosure/DialogInstanceContext.js.map +1 -1
  8. package/dist/components/feedback/busy/BusyContext.d.ts +18 -0
  9. package/dist/components/feedback/busy/BusyContext.d.ts.map +1 -1
  10. package/dist/components/feedback/busy/BusyContext.js.map +1 -1
  11. package/dist/components/feedback/busy/BusyProvider.d.ts +10 -0
  12. package/dist/components/feedback/busy/BusyProvider.d.ts.map +1 -1
  13. package/dist/components/feedback/busy/BusyProvider.js.map +1 -1
  14. package/dist/components/feedback/notification/NotificationContext.d.ts +29 -0
  15. package/dist/components/feedback/notification/NotificationContext.d.ts.map +1 -1
  16. package/dist/components/feedback/notification/NotificationContext.js.map +1 -1
  17. package/dist/components/feedback/notification/NotificationProvider.d.ts +9 -0
  18. package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
  19. package/dist/components/feedback/notification/NotificationProvider.js.map +1 -1
  20. package/dist/hooks/useLogger.d.ts +4 -2
  21. package/dist/hooks/useLogger.d.ts.map +1 -1
  22. package/dist/hooks/useLogger.js +11 -4
  23. package/dist/hooks/useLogger.js.map +1 -1
  24. package/dist/hooks/useSyncConfig.d.ts +2 -0
  25. package/dist/hooks/useSyncConfig.d.ts.map +1 -1
  26. package/dist/hooks/useSyncConfig.js +30 -26
  27. package/dist/hooks/useSyncConfig.js.map +1 -1
  28. package/dist/index.d.ts +8 -14
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +21 -15
  31. package/dist/index.js.map +1 -1
  32. package/dist/providers/InitializeProvider.d.ts +33 -0
  33. package/dist/providers/InitializeProvider.d.ts.map +1 -0
  34. package/dist/providers/InitializeProvider.js +75 -0
  35. package/dist/providers/InitializeProvider.js.map +6 -0
  36. package/dist/providers/LoggerContext.d.ts +24 -8
  37. package/dist/providers/LoggerContext.d.ts.map +1 -1
  38. package/dist/providers/LoggerContext.js +13 -13
  39. package/dist/providers/LoggerContext.js.map +2 -2
  40. package/dist/providers/ServiceClientContext.d.ts +13 -0
  41. package/dist/providers/ServiceClientContext.d.ts.map +1 -1
  42. package/dist/providers/ServiceClientContext.js.map +1 -1
  43. package/dist/providers/ServiceClientProvider.d.ts +21 -0
  44. package/dist/providers/ServiceClientProvider.d.ts.map +1 -1
  45. package/dist/providers/ServiceClientProvider.js.map +1 -1
  46. package/dist/providers/SyncStorageContext.d.ts +25 -11
  47. package/dist/providers/SyncStorageContext.d.ts.map +1 -1
  48. package/dist/providers/SyncStorageContext.js +13 -13
  49. package/dist/providers/SyncStorageContext.js.map +2 -2
  50. package/dist/providers/shared-data/SharedDataChangeEvent.d.ts +8 -0
  51. package/dist/providers/shared-data/SharedDataChangeEvent.d.ts.map +1 -1
  52. package/dist/providers/shared-data/SharedDataChangeEvent.js.map +1 -1
  53. package/dist/providers/shared-data/SharedDataContext.d.ts +39 -0
  54. package/dist/providers/shared-data/SharedDataContext.d.ts.map +1 -1
  55. package/dist/providers/shared-data/SharedDataContext.js +1 -3
  56. package/dist/providers/shared-data/SharedDataContext.js.map +1 -1
  57. package/dist/providers/shared-data/SharedDataProvider.d.ts +30 -5
  58. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  59. package/dist/providers/shared-data/SharedDataProvider.js +59 -38
  60. package/dist/providers/shared-data/SharedDataProvider.js.map +2 -2
  61. package/docs/providers.md +70 -195
  62. package/package.json +3 -3
  63. package/src/components/disclosure/DialogContext.ts +29 -0
  64. package/src/components/disclosure/DialogInstanceContext.ts +14 -0
  65. package/src/components/feedback/busy/BusyContext.ts +18 -0
  66. package/src/components/feedback/busy/BusyProvider.tsx +10 -0
  67. package/src/components/feedback/notification/NotificationContext.ts +29 -0
  68. package/src/components/feedback/notification/NotificationProvider.tsx +9 -0
  69. package/src/hooks/useLogger.ts +14 -4
  70. package/src/hooks/useSyncConfig.ts +42 -35
  71. package/src/index.ts +34 -14
  72. package/src/providers/InitializeProvider.tsx +74 -0
  73. package/src/providers/LoggerContext.tsx +39 -10
  74. package/src/providers/ServiceClientContext.ts +13 -0
  75. package/src/providers/ServiceClientProvider.tsx +21 -0
  76. package/src/providers/SyncStorageContext.tsx +40 -15
  77. package/src/providers/shared-data/SharedDataChangeEvent.ts +8 -0
  78. package/src/providers/shared-data/SharedDataContext.ts +40 -3
  79. package/src/providers/shared-data/SharedDataProvider.tsx +102 -54
@@ -8,6 +8,27 @@ import { ServiceClientContext, type ServiceClientContextValue } from "./ServiceC
8
8
  import { useConfig } from "./ConfigContext";
9
9
  import { useNotification } from "../components/feedback/notification/NotificationContext";
10
10
 
11
+ /**
12
+ * WebSocket 서비스 클라이언트 Provider
13
+ *
14
+ * @remarks
15
+ * - ConfigProvider와 NotificationProvider 내부에서 사용해야 함
16
+ * - key 기반 다중 연결 관리
17
+ * - 요청/응답 진행률을 NotificationProvider 알림으로 표시
18
+ * - host, port, ssl 미지정 시 window.location에서 자동 추론
19
+ * - cleanup 시 모든 연결 자동 종료
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * <ConfigProvider clientName="my-app">
24
+ * <NotificationProvider>
25
+ * <ServiceClientProvider>
26
+ * <App />
27
+ * </ServiceClientProvider>
28
+ * </NotificationProvider>
29
+ * </ConfigProvider>
30
+ * ```
31
+ */
11
32
  export const ServiceClientProvider: ParentComponent = (props) => {
12
33
  const config = useConfig();
13
34
  const notification = useNotification();
@@ -1,4 +1,10 @@
1
- import { createContext, useContext, type ParentComponent } from "solid-js";
1
+ import {
2
+ type Accessor,
3
+ createContext,
4
+ createSignal,
5
+ useContext,
6
+ type ParentComponent,
7
+ } from "solid-js";
2
8
 
3
9
  /**
4
10
  * 커스텀 동기화 저장소 어댑터 인터페이스
@@ -13,40 +19,59 @@ export interface StorageAdapter {
13
19
  removeItem(key: string): void | Promise<void>;
14
20
  }
15
21
 
22
+ /**
23
+ * 동기화 저장소 Context 값
24
+ *
25
+ * @remarks
26
+ * - `adapter`: 현재 설정된 StorageAdapter (signal). configure 전에는 undefined
27
+ * - `configure`: adapter를 나중에 주입하는 함수
28
+ */
29
+ export interface SyncStorageContextValue {
30
+ adapter: Accessor<StorageAdapter | undefined>;
31
+ configure: (adapter: StorageAdapter) => void;
32
+ }
33
+
16
34
  /**
17
35
  * 동기화 저장소 Context
18
36
  *
19
37
  * @remarks
20
38
  * Provider가 없으면 `undefined` (useSyncConfig에서 localStorage로 fallback)
21
39
  */
22
- export const SyncStorageContext = createContext<StorageAdapter | undefined>(undefined);
40
+ export const SyncStorageContext = createContext<SyncStorageContextValue>();
23
41
 
24
42
  /**
25
43
  * 동기화 저장소 Context에 접근하는 훅
26
44
  *
27
- * @returns StorageAdapter 또는 undefined (Provider가 없으면)
45
+ * @returns SyncStorageContextValue 또는 undefined (Provider가 없으면)
28
46
  */
29
- export function useSyncStorage(): StorageAdapter | undefined {
47
+ export function useSyncStorage(): SyncStorageContextValue | undefined {
30
48
  return useContext(SyncStorageContext);
31
49
  }
32
50
 
33
51
  /**
34
52
  * 동기화 저장소 Provider
35
53
  *
54
+ * @remarks
55
+ * - prop 없이 사용. adapter는 `useSyncStorage().configure()`로 나중에 주입
56
+ * - configure 전에는 useSyncConfig이 localStorage로 fallback
57
+ *
36
58
  * @example
37
59
  * ```tsx
38
- * <SyncStorageProvider storage={myStorageAdapter}>
39
- * <ThemeProvider>
40
- * <App />
41
- * </ThemeProvider>
60
+ * <SyncStorageProvider>
61
+ * <App />
42
62
  * </SyncStorageProvider>
63
+ *
64
+ * // 자식 컴포넌트에서 나중에 설정:
65
+ * useSyncStorage()!.configure(myStorageAdapter);
43
66
  * ```
44
67
  */
45
- export const SyncStorageProvider: ParentComponent<{ storage: StorageAdapter }> = (props) => {
46
- return (
47
- // eslint-disable-next-line solid/reactivity -- storage는 초기 설정값으로 변경되지 않음
48
- <SyncStorageContext.Provider value={props.storage}>
49
- {props.children}
50
- </SyncStorageContext.Provider>
51
- );
68
+ export const SyncStorageProvider: ParentComponent = (props) => {
69
+ const [adapter, setAdapter] = createSignal<StorageAdapter | undefined>();
70
+
71
+ const value: SyncStorageContextValue = {
72
+ adapter,
73
+ configure: (a: StorageAdapter) => setAdapter(() => a),
74
+ };
75
+
76
+ return <SyncStorageContext.Provider value={value}>{props.children}</SyncStorageContext.Provider>;
52
77
  };
@@ -1,5 +1,13 @@
1
1
  import { defineEvent } from "@simplysm/service-common";
2
2
 
3
+ /**
4
+ * SharedData 변경 이벤트 정의
5
+ *
6
+ * @remarks
7
+ * 서버-클라이언트 간 공유 데이터 변경을 알리는 이벤트.
8
+ * - 이벤트 정보: `{ name: string; filter: unknown }` — 데이터 이름과 필터
9
+ * - 이벤트 데이터: `(string | number)[] | undefined` — 변경된 항목의 key 배열 (undefined면 전체 갱신)
10
+ */
3
11
  export const SharedDataChangeEvent = defineEvent<
4
12
  { name: string; filter: unknown },
5
13
  (string | number)[] | undefined
@@ -1,36 +1,73 @@
1
1
  import { type Accessor, createContext, useContext } from "solid-js";
2
2
 
3
+ /**
4
+ * 공유 데이터 정의
5
+ *
6
+ * @remarks
7
+ * SharedDataProvider에 전달하여 서버 데이터 구독을 설정한다.
8
+ */
3
9
  export interface SharedDataDefinition<TData> {
10
+ /** 서비스 연결 key (useServiceClient의 connect key와 동일) */
4
11
  serviceKey: string;
12
+ /** 데이터 조회 함수 (changeKeys가 있으면 해당 항목만 부분 갱신) */
5
13
  fetch: (changeKeys?: Array<string | number>) => Promise<TData[]>;
14
+ /** 항목의 고유 key 추출 함수 */
6
15
  getKey: (item: TData) => string | number;
16
+ /** 정렬 기준 배열 (여러 기준 적용 가능) */
7
17
  orderBy: [(item: TData) => unknown, "asc" | "desc"][];
18
+ /** 서버 이벤트 필터 (같은 name의 이벤트 중 filter가 일치하는 것만 수신) */
8
19
  filter?: unknown;
9
20
  }
10
21
 
22
+ /**
23
+ * 공유 데이터 접근자
24
+ *
25
+ * @remarks
26
+ * 각 데이터 key에 대한 반응형 접근 및 변경 알림을 제공한다.
27
+ */
11
28
  export interface SharedDataAccessor<TData> {
29
+ /** 반응형 항목 배열 */
12
30
  items: Accessor<TData[]>;
31
+ /** key로 단일 항목 조회 */
13
32
  get: (key: string | number | undefined) => TData | undefined;
33
+ /** 서버에 변경 이벤트 전파 (모든 구독자에게 refetch 트리거) */
14
34
  emit: (changeKeys?: Array<string | number>) => Promise<void>;
15
35
  }
16
36
 
37
+ /**
38
+ * 공유 데이터 Context 값
39
+ *
40
+ * @remarks
41
+ * - configure 호출 전: wait, busy, configure만 접근 가능. 데이터 접근 시 throw
42
+ * - configure 호출 후: 각 데이터 key별 SharedDataAccessor와 전체 상태 관리 메서드 포함
43
+ */
17
44
  export type SharedDataValue<TSharedData extends Record<string, unknown>> = {
18
45
  [K in keyof TSharedData]: SharedDataAccessor<TSharedData[K]>;
19
46
  } & {
47
+ /** 모든 초기 fetch 완료까지 대기 */
20
48
  wait: () => Promise<void>;
49
+ /** fetch 진행 중 여부 */
21
50
  busy: Accessor<boolean>;
51
+ /** definitions를 설정하여 데이터 구독 시작 */
52
+ configure: (definitions: {
53
+ [K in keyof TSharedData]: SharedDataDefinition<TSharedData[K]>;
54
+ }) => void;
22
55
  };
23
56
 
57
+ /** 공유 데이터 Context */
24
58
  export const SharedDataContext = createContext<SharedDataValue<Record<string, unknown>>>();
25
59
 
60
+ /**
61
+ * 공유 데이터에 접근하는 훅
62
+ *
63
+ * @throws SharedDataProvider가 없으면 에러 발생
64
+ */
26
65
  export function useSharedData<
27
66
  TSharedData extends Record<string, unknown> = Record<string, unknown>,
28
67
  >(): SharedDataValue<TSharedData> {
29
68
  const context = useContext(SharedDataContext);
30
69
  if (!context) {
31
- throw new Error(
32
- "useSharedData는 SharedDataProvider 내부에서만 사용할 수 있습니다. SharedDataProvider는 ServiceClientProvider 아래에 위치해야 합니다",
33
- );
70
+ throw new Error("useSharedData는 SharedDataProvider 내부에서만 사용할 수 있습니다");
34
71
  }
35
72
  return context as unknown as SharedDataValue<TSharedData>;
36
73
  }
@@ -11,14 +11,41 @@ import { useServiceClient } from "../ServiceClientContext";
11
11
  import { useNotification } from "../../components/feedback/notification/NotificationContext";
12
12
  import { useLogger } from "../../hooks/useLogger";
13
13
 
14
- export function SharedDataProvider<TSharedData extends Record<string, unknown>>(props: {
15
- definitions: { [K in keyof TSharedData]: SharedDataDefinition<TSharedData[K]> };
16
- children: JSX.Element;
17
- }): JSX.Element {
14
+ /**
15
+ * 공유 데이터 Provider
16
+ *
17
+ * @remarks
18
+ * - ServiceClientProvider와 NotificationProvider 내부에서 사용해야 함
19
+ * - LoggerProvider가 있으면 fetch 실패를 로거에도 기록
20
+ * - configure() 호출 전: wait, busy, configure만 접근 가능. 데이터 접근 시 throw
21
+ * - configure() 호출 후: definitions의 각 key마다 서버 이벤트 리스너를 등록하여 실시간 동기화
22
+ * - 동시 fetch 호출 시 version counter로 데이터 역전 방지
23
+ * - fetch 실패 시 사용자에게 danger 알림 표시
24
+ * - cleanup 시 모든 이벤트 리스너 자동 해제
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <SharedDataProvider>
29
+ * <App />
30
+ * </SharedDataProvider>
31
+ *
32
+ * // 자식 컴포넌트에서 나중에 설정:
33
+ * useSharedData().configure({
34
+ * users: {
35
+ * serviceKey: "main",
36
+ * fetch: async (changeKeys) => fetchUsers(changeKeys),
37
+ * getKey: (item) => item.id,
38
+ * orderBy: [[(item) => item.name, "asc"]],
39
+ * },
40
+ * });
41
+ * ```
42
+ */
43
+ export function SharedDataProvider(props: { children: JSX.Element }): JSX.Element {
18
44
  const serviceClient = useServiceClient();
19
45
  const notification = useNotification();
20
46
  const logger = useLogger();
21
47
 
48
+ let configured = false;
22
49
  const [busyCount, setBusyCount] = createSignal(0);
23
50
  const busy: Accessor<boolean> = () => busyCount() > 0;
24
51
 
@@ -26,6 +53,8 @@ export function SharedDataProvider<TSharedData extends Record<string, unknown>>(
26
53
  const memoMap = new Map<string, Accessor<Map<string | number, unknown>>>();
27
54
  const listenerKeyMap = new Map<string, string>();
28
55
  const versionMap = new Map<string, number>();
56
+ const accessors: Record<string, SharedDataAccessor<unknown>> = {};
57
+ let currentDefinitions: Record<string, SharedDataDefinition<unknown>> | undefined;
29
58
 
30
59
  function ordering<TT>(data: TT[], orderByList: [(item: TT) => unknown, "asc" | "desc"][]): TT[] {
31
60
  let result = [...data];
@@ -86,70 +115,89 @@ export function SharedDataProvider<TSharedData extends Record<string, unknown>>(
86
115
  await waitUntil(() => busyCount() <= 0);
87
116
  }
88
117
 
89
- const accessors: Record<string, SharedDataAccessor<unknown>> = {};
90
-
91
- // eslint-disable-next-line solid/reactivity -- definitions초기 설정용으로 마운트 시 1회만 읽음
92
- for (const [name, def] of Object.entries(props.definitions) as [
93
- string,
94
- SharedDataDefinition<unknown>,
95
- ][]) {
96
- const [items, setItems] = createSignal<unknown[]>([]);
97
- // eslint-disable-next-line solid/reactivity -- signal 참조를 Map에 저장하는 것은 반응성 접근이 아님
98
- signalMap.set(name, [items, setItems]);
99
-
100
- const itemMap = createMemo(() => {
101
- const map = new Map<string | number, unknown>();
102
- for (const item of items()) {
103
- map.set(def.getKey(item as never), item);
104
- }
105
- return map;
106
- });
107
- // eslint-disable-next-line solid/reactivity -- memo 참조를 Map에 저장하는 것은 반응성 접근이 아님
108
- memoMap.set(name, itemMap);
109
-
110
- const client = serviceClient.get(def.serviceKey);
111
- void client
112
- .addEventListener(SharedDataChangeEvent, { name, filter: def.filter }, async (changeKeys) => {
113
- await loadData(name, def, changeKeys);
114
- })
115
- .then((key) => {
116
- listenerKeyMap.set(name, key);
118
+ function configure(definitions: Record<string, SharedDataDefinition<unknown>>): void {
119
+ if (configured) {
120
+ throw new Error("SharedDataProvider: configure()1회만 호출할 있습니다");
121
+ }
122
+ configured = true;
123
+ currentDefinitions = definitions;
124
+
125
+ for (const [name, def] of Object.entries(definitions)) {
126
+ const [items, setItems] = createSignal<unknown[]>([]);
127
+ // eslint-disable-next-line solid/reactivity -- signal 참조를 Map에 저장하는 것은 반응성 접근이 아님
128
+ signalMap.set(name, [items, setItems]);
129
+
130
+ const itemMap = createMemo(() => {
131
+ const map = new Map<string | number, unknown>();
132
+ for (const item of items()) {
133
+ map.set(def.getKey(item as never), item);
134
+ }
135
+ return map;
117
136
  });
137
+ // eslint-disable-next-line solid/reactivity -- memo 참조를 Map에 저장하는 것은 반응성 접근이 아님
138
+ memoMap.set(name, itemMap);
118
139
 
119
- void loadData(name, def);
120
-
121
- accessors[name] = {
122
- items,
123
- get: (key: string | number | undefined) => {
124
- if (key === undefined) return undefined;
125
- return itemMap().get(key);
126
- },
127
- emit: async (changeKeys?: Array<string | number>) => {
128
- await client.emitToServer(
140
+ const client = serviceClient.get(def.serviceKey);
141
+ void client
142
+ .addEventListener(
129
143
  SharedDataChangeEvent,
130
- (info) => info.name === name && objEqual(info.filter, def.filter),
131
- changeKeys,
132
- );
133
- },
134
- };
144
+ { name, filter: def.filter },
145
+ async (changeKeys) => {
146
+ await loadData(name, def, changeKeys);
147
+ },
148
+ )
149
+ .then((key) => {
150
+ listenerKeyMap.set(name, key);
151
+ });
152
+
153
+ void loadData(name, def);
154
+
155
+ accessors[name] = {
156
+ items,
157
+ get: (key: string | number | undefined) => {
158
+ if (key === undefined) return undefined;
159
+ return itemMap().get(key);
160
+ },
161
+ emit: async (changeKeys?: Array<string | number>) => {
162
+ await client.emitToServer(
163
+ SharedDataChangeEvent,
164
+ (info) => info.name === name && objEqual(info.filter, def.filter),
165
+ changeKeys,
166
+ );
167
+ },
168
+ };
169
+ }
135
170
  }
136
171
 
137
172
  onCleanup(() => {
138
- for (const [name] of Object.entries(props.definitions)) {
173
+ if (!currentDefinitions) return;
174
+ for (const [name] of Object.entries(currentDefinitions)) {
139
175
  const listenerKey = listenerKeyMap.get(name);
140
176
  if (listenerKey != null) {
141
- const def = (props.definitions as Record<string, SharedDataDefinition<unknown>>)[name];
177
+ const def = currentDefinitions[name];
142
178
  const client = serviceClient.get(def.serviceKey);
143
179
  void client.removeEventListener(listenerKey);
144
180
  }
145
181
  }
146
182
  });
147
183
 
148
- const contextValue = {
149
- ...accessors,
150
- wait,
151
- busy,
152
- } as SharedDataValue<Record<string, unknown>>;
184
+ const KNOWN_KEYS = new Set(["wait", "busy", "configure"]);
185
+
186
+ // Proxy: configure 전 데이터 접근 시 throw
187
+ const contextValue = new Proxy(
188
+ { wait, busy, configure } as SharedDataValue<Record<string, unknown>>,
189
+ {
190
+ get(target, prop: string) {
191
+ if (KNOWN_KEYS.has(prop)) {
192
+ return target[prop];
193
+ }
194
+ if (!configured) {
195
+ throw new Error("SharedDataProvider: configure()를 먼저 호출해야 합니다");
196
+ }
197
+ return accessors[prop];
198
+ },
199
+ },
200
+ );
153
201
 
154
202
  return (
155
203
  <SharedDataContext.Provider value={contextValue}>{props.children}</SharedDataContext.Provider>