@dooboostore/simple-web-component 1.0.53 → 1.0.55

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 (121) hide show
  1. package/README.md +308 -56
  2. package/dist/cjs/decorators/addEventListener.js +37 -14
  3. package/dist/cjs/decorators/addEventListener.js.map +2 -2
  4. package/dist/cjs/decorators/applyAttribute.js +6 -3
  5. package/dist/cjs/decorators/applyAttribute.js.map +2 -2
  6. package/dist/cjs/decorators/applyClass.js +18 -2
  7. package/dist/cjs/decorators/applyClass.js.map +2 -2
  8. package/dist/cjs/decorators/applyNode.js +24 -2
  9. package/dist/cjs/decorators/applyNode.js.map +2 -2
  10. package/dist/cjs/decorators/changedAttribute.js +2 -5
  11. package/dist/cjs/decorators/changedAttribute.js.map +2 -2
  12. package/dist/cjs/decorators/elementDefine.js +13 -2
  13. package/dist/cjs/decorators/elementDefine.js.map +2 -2
  14. package/dist/cjs/decorators/eventMedia.js +2 -5
  15. package/dist/cjs/decorators/eventMedia.js.map +2 -2
  16. package/dist/cjs/decorators/fetch.js +216 -82
  17. package/dist/cjs/decorators/fetch.js.map +3 -3
  18. package/dist/cjs/decorators/index.js +1 -0
  19. package/dist/cjs/decorators/index.js.map +2 -2
  20. package/dist/cjs/decorators/intersectionObserver.js +3 -6
  21. package/dist/cjs/decorators/intersectionObserver.js.map +2 -2
  22. package/dist/cjs/decorators/mutationObserver.js +3 -6
  23. package/dist/cjs/decorators/mutationObserver.js.map +2 -2
  24. package/dist/cjs/decorators/parameter.js +197 -33
  25. package/dist/cjs/decorators/parameter.js.map +3 -3
  26. package/dist/cjs/decorators/persistState.js +474 -0
  27. package/dist/cjs/decorators/persistState.js.map +7 -0
  28. package/dist/cjs/decorators/resizeObserver.js +3 -6
  29. package/dist/cjs/decorators/resizeObserver.js.map +2 -2
  30. package/dist/cjs/decorators/setInterval.js +2 -5
  31. package/dist/cjs/decorators/setInterval.js.map +2 -2
  32. package/dist/cjs/decorators/setTimeout.js +2 -5
  33. package/dist/cjs/decorators/setTimeout.js.map +2 -2
  34. package/dist/cjs/decorators/subscribeSwcAppMessage.js.map +1 -1
  35. package/dist/cjs/elements/SwcAppMixin.js +50 -2
  36. package/dist/cjs/elements/SwcAppMixin.js.map +2 -2
  37. package/dist/esm/decorators/addEventListener.js +38 -15
  38. package/dist/esm/decorators/addEventListener.js.map +2 -2
  39. package/dist/esm/decorators/applyAttribute.js +6 -3
  40. package/dist/esm/decorators/applyAttribute.js.map +2 -2
  41. package/dist/esm/decorators/applyClass.js +18 -2
  42. package/dist/esm/decorators/applyClass.js.map +2 -2
  43. package/dist/esm/decorators/applyNode.js +24 -2
  44. package/dist/esm/decorators/applyNode.js.map +2 -2
  45. package/dist/esm/decorators/changedAttribute.js +3 -6
  46. package/dist/esm/decorators/changedAttribute.js.map +2 -2
  47. package/dist/esm/decorators/elementDefine.js +15 -4
  48. package/dist/esm/decorators/elementDefine.js.map +2 -2
  49. package/dist/esm/decorators/eventMedia.js +3 -6
  50. package/dist/esm/decorators/eventMedia.js.map +2 -2
  51. package/dist/esm/decorators/fetch.js +216 -82
  52. package/dist/esm/decorators/fetch.js.map +3 -3
  53. package/dist/esm/decorators/index.js +1 -0
  54. package/dist/esm/decorators/index.js.map +2 -2
  55. package/dist/esm/decorators/intersectionObserver.js +4 -7
  56. package/dist/esm/decorators/intersectionObserver.js.map +2 -2
  57. package/dist/esm/decorators/mutationObserver.js +4 -7
  58. package/dist/esm/decorators/mutationObserver.js.map +2 -2
  59. package/dist/esm/decorators/parameter.js +197 -33
  60. package/dist/esm/decorators/parameter.js.map +3 -3
  61. package/dist/esm/decorators/persistState.js +455 -0
  62. package/dist/esm/decorators/persistState.js.map +7 -0
  63. package/dist/esm/decorators/resizeObserver.js +4 -7
  64. package/dist/esm/decorators/resizeObserver.js.map +2 -2
  65. package/dist/esm/decorators/setInterval.js +3 -6
  66. package/dist/esm/decorators/setInterval.js.map +2 -2
  67. package/dist/esm/decorators/setTimeout.js +3 -6
  68. package/dist/esm/decorators/setTimeout.js.map +2 -2
  69. package/dist/esm/decorators/subscribeSwcAppMessage.js.map +1 -1
  70. package/dist/esm/elements/SwcAppMixin.js +51 -3
  71. package/dist/esm/elements/SwcAppMixin.js.map +2 -2
  72. package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +4470 -3620
  73. package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
  74. package/dist/types/decorators/addEventListener.d.ts +15 -0
  75. package/dist/types/decorators/addEventListener.d.ts.map +1 -1
  76. package/dist/types/decorators/applyAttribute.d.ts +1 -0
  77. package/dist/types/decorators/applyAttribute.d.ts.map +1 -1
  78. package/dist/types/decorators/applyClass.d.ts.map +1 -1
  79. package/dist/types/decorators/applyNode.d.ts +14 -0
  80. package/dist/types/decorators/applyNode.d.ts.map +1 -1
  81. package/dist/types/decorators/changedAttribute.d.ts.map +1 -1
  82. package/dist/types/decorators/elementDefine.d.ts.map +1 -1
  83. package/dist/types/decorators/eventMedia.d.ts.map +1 -1
  84. package/dist/types/decorators/fetch.d.ts +52 -31
  85. package/dist/types/decorators/fetch.d.ts.map +1 -1
  86. package/dist/types/decorators/index.d.ts +1 -0
  87. package/dist/types/decorators/index.d.ts.map +1 -1
  88. package/dist/types/decorators/intersectionObserver.d.ts.map +1 -1
  89. package/dist/types/decorators/mutationObserver.d.ts.map +1 -1
  90. package/dist/types/decorators/parameter.d.ts +169 -31
  91. package/dist/types/decorators/parameter.d.ts.map +1 -1
  92. package/dist/types/decorators/persistState.d.ts +66 -0
  93. package/dist/types/decorators/persistState.d.ts.map +1 -0
  94. package/dist/types/decorators/resizeObserver.d.ts.map +1 -1
  95. package/dist/types/decorators/setInterval.d.ts.map +1 -1
  96. package/dist/types/decorators/setTimeout.d.ts.map +1 -1
  97. package/dist/types/decorators/subscribeSwcAppMessage.d.ts +1 -1
  98. package/dist/types/decorators/subscribeSwcAppMessage.d.ts.map +1 -1
  99. package/dist/types/elements/SwcAppMixin.d.ts +12 -0
  100. package/dist/types/elements/SwcAppMixin.d.ts.map +1 -1
  101. package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +4470 -3620
  102. package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
  103. package/package.json +1 -1
  104. package/src/decorators/addEventListener.ts +23 -15
  105. package/src/decorators/applyAttribute.ts +8 -3
  106. package/src/decorators/applyClass.ts +22 -2
  107. package/src/decorators/applyNode.ts +44 -2
  108. package/src/decorators/changedAttribute.ts +3 -4
  109. package/src/decorators/elementDefine.ts +19 -7
  110. package/src/decorators/eventMedia.ts +3 -6
  111. package/src/decorators/fetch.ts +270 -140
  112. package/src/decorators/index.ts +1 -0
  113. package/src/decorators/intersectionObserver.ts +4 -5
  114. package/src/decorators/mutationObserver.ts +4 -5
  115. package/src/decorators/parameter.ts +326 -42
  116. package/src/decorators/persistState.ts +408 -0
  117. package/src/decorators/resizeObserver.ts +4 -5
  118. package/src/decorators/setInterval.ts +3 -4
  119. package/src/decorators/setTimeout.ts +3 -4
  120. package/src/decorators/subscribeSwcAppMessage.ts +1 -1
  121. package/src/elements/SwcAppMixin.ts +64 -5
@@ -25,45 +25,24 @@ export type FetchManualFn = (
25
25
  returnValue?: any
26
26
  ) => any | Promise<any>;
27
27
 
28
- /** 모드(선언형/수동형, before/after)와 무관한 공통 옵션 */
28
+ /** filter 가 받는 것 */
29
+ export type FetchFilterContext = { self: any; helper: HelperHostSet; args: any[] };
30
+
31
+ /**
32
+ * 모드(선언형/수동형, before/after)와 무관한 공통 옵션.
33
+ * 결과 처리(then/catch/finally)는 @fetchPromise 로 받은 Promise 로 사용자가 직접 한다 — 그래서 훅이 없다.
34
+ */
29
35
  export type FetchType = {
30
36
  /**
31
37
  * 같은 메서드를 다시 부르면 진행 중인 이전 요청을 abort 한다 (검색 자동완성 등 마지막 요청만 살림).
32
- * 끊긴 이전 호출은 조용히 undefined 로 끝난다.
38
+ * 끊긴 이전 호출의 Promise 는 끝나지 않는다 (떨어진 DOM 에 그리지 않고 조용히 끝남).
33
39
  */
34
40
  abortPrevious?: boolean;
35
- /** false 를 리턴하면 이번 호출을 통째로 건너뛴다 (메서드도 fetch 도 실행 안 함, undefined 리턴). */
36
- filter?: (
37
- currentThis: any,
38
- helper: HelperHostSet,
39
- parameter: any[]
40
- ) => boolean | Promise<boolean>;
41
- /** filter 통과 후 가장 먼저 실행 (await). 리턴값은 무시. init 조립은 request 팩토리 담당. */
42
- before?: (
43
- currentThis: any,
44
- helper: HelperHostSet,
45
- parameter: any[]
46
- ) => void | Promise<void>;
47
41
  /**
48
- * fetch settled 후 호출 (fulfilled/rejected 모두). 호출만 하고 리턴값은 무시.
49
- * 변환·복구가 필요하면 manual 로 직접 구현할 것.
42
+ * false 를 리턴하면 이번 호출을 통째로 건너뛴다 (요청도 메서드도 실행 안 함, undefined 리턴).
43
+ * 요청이 메서드보다 먼저 나가므로 "보낼지 말지"는 여기서만 정할 수 있다. 메서드를 동기로 유지하려고 동기만 받는다.
50
44
  */
51
- after?: (
52
- currentThis: any,
53
- helper: HelperHostSet,
54
- parameter: any[],
55
- settled: PromiseSettledResult<any>
56
- ) => void | Promise<void>;
57
- /**
58
- * 성공/실패 무관 항상 실행 (다른 데코레이터의 finally 와 같은 규칙). 에러는 삼키지 않고 전파.
59
- * args: 메서드에 넘긴 인자, result: 이번 호출의 리턴값(before 모드 = 메서드 리턴, after 모드 = fetch 결과)
60
- */
61
- finally?: (
62
- currentThis: any,
63
- helper: HelperHostSet,
64
- parameter: any[],
65
- ctx: { args: any[]; result?: any; error?: any }
66
- ) => any | Promise<any>;
45
+ filter?: (ctx: FetchFilterContext) => boolean;
67
46
  };
68
47
 
69
48
  /** before: 인자 기반 RequestInit 조립. 없으면 GET. */
@@ -84,8 +63,13 @@ export type ReturnRequestFactory = (
84
63
  export interface FetchMetadata {
85
64
  propertyKey: string | symbol;
86
65
  options: FetchOptions;
66
+ /** 필드(declare)에 붙었으면 true — 프레임워크가 요청 함수를 만든다 */
67
+ field?: boolean;
87
68
  }
88
69
 
70
+ // 프레임워크가 만든 필드 요청 함수 표시 (덮어써졌는지 확인용)
71
+ const FETCH_FIELD_FN = Symbol.for('simple-web-component:fetch-field-fn');
72
+
89
73
  /** 선언형: url/request/process 로 기술하면 프레임워크가 fetch 한다 */
90
74
  export type FetchDeclarative<R> = {
91
75
  url: RequestInfo | URL | FetchUrlFactory;
@@ -110,8 +94,8 @@ export type FetchManual = {
110
94
  };
111
95
 
112
96
  export type FetchOptions =
113
- // before(기본): (선언형 fetch | manual) → settled를 @fetchSettled 자리에
114
- // 주입해 메서드 실행. 메서드 결과 리턴. request 없으면 GET. valueKey 는 after 전용이라 금지.
97
+ // before(기본): 요청을 시작하고 그 Promise 를 @fetchPromise 자리에 넣어 메서드를 바로 호출한다.
98
+ // 메서드 반환값을 그대로 리턴 (동기 메서드는 동기 그대로). request 없으면 GET. valueKey 는 after 전용이라 금지.
115
99
  | (FetchType & { trigger?: 'before'; valueKey?: never } & (FetchDeclarative<RequestFactory> | FetchManual))
116
100
  // after: 메서드 실행 → 리턴값(valueKey 가 있으면 returnValue[valueKey])을 body(수동형이면 returnValue)로 → fetch. fetch 결과 리턴.
117
101
  // (valueKey 기본값 = FETCH_METADATA_KEY: Symbol 키 우선 조회, 없으면 통째 값)
@@ -141,19 +125,44 @@ const combineSignals = (user: AbortSignal | null | undefined, own: AbortSignal):
141
125
  return merged.signal;
142
126
  };
143
127
 
144
- function createFetch(optionsOrUrl: FetchOptions | string): MethodDecorator {
128
+ /**
129
+ * @fetch 계열 데코레이터는 메서드와 필드 양쪽에 붙는다.
130
+ * - 메서드: 트리거가 부르는 핸들러용. 요청 Promise 를 @fetchPromise 로 주입하고 메서드를 쌓는다 (기존 방식)
131
+ * - 필드(declare): 직접 부르는 요청 함수용. `declare getPost: (id: number) => Promise<Post>` 의 몸통을 프레임워크가 만든다
132
+ */
133
+ export type FetchDecorator = {
134
+ /**
135
+ * 필드: 선언 타입이 `(...args) => Promise<…>`(요청 함수) 또는 `Promise<…>`(값) 가 아니면 컴파일 에러.
136
+ * 옵셔널(`declare me?: Promise<T>`)도 된다 — 값 필드를 `this.me = undefined` 로 비우려면 strict 에선 옵셔널로 선언한다.
137
+ * (private / protected 필드는 TS 가 바깥 타입에서 못 봐서 검사 없이 통과 — 런타임 design:type 검사만 적용)
138
+ */
139
+ <T, K extends string | symbol>(target: T & (K extends keyof T ? { [P in K]?: ((...args: any[]) => Promise<any>) | Promise<any> } : unknown), propertyKey: K): void;
140
+ /** 메서드: 트리거 핸들러 (요청 Promise 는 @fetchPromise 로) */
141
+ (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): any;
142
+ };
143
+
144
+ // 건너뛰거나 끊긴 필드 호출이 돌려주는 Promise — 끝나지 않는다 (떨어진 DOM 에 그리지 않고 에러도 없음)
145
+ const never = () => new Promise<never>(() => {});
146
+
147
+ // 필드를 Promise 로 쓸 때 기억한 요청 — 엘리먼트 → (필드 이름 → Promise). 엘리먼트에 속성을 붙이지 않는다.
148
+ const valueCache = new WeakMap<object, Map<string | symbol, Promise<any>>>();
149
+ const cacheOf = (inst: object) => {
150
+ let m = valueCache.get(inst);
151
+ if (!m) valueCache.set(inst, (m = new Map()));
152
+ return m;
153
+ };
154
+
155
+ function createFetch(optionsOrUrl: FetchOptions | string): FetchDecorator {
145
156
  const options: FetchOptions = typeof optionsOrUrl === 'string' ? { url: optionsOrUrl } : optionsOrUrl;
146
- return (target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor) => {
157
+ return ((target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor) => {
147
158
  const constructor = target.constructor;
159
+ const isField = !descriptor;
148
160
  const list = (ReflectUtils.getOwnMetadata(FETCH_METADATA_KEY, constructor) as FetchMetadata[] | undefined) ?? [];
149
- list.push({ propertyKey, options });
161
+ list.push({ propertyKey, options, field: isField });
150
162
  ReflectUtils.defineMetadata(FETCH_METADATA_KEY, list, constructor);
151
163
 
152
- if (!descriptor) return descriptor;
153
- const originalMethod = descriptor.value;
154
-
155
164
  const extractBody = (v: any) => {
156
- const keyToUse = (options as { valueKey?: symbol | string }).valueKey ?? FETCH_METADATA_KEY;
165
+ const keyToUse = (options as { valueKey?: symbol | string }).valueKey ?? FETCH_METADATA_KEY;
157
166
  if (v && typeof v === 'object' && keyToUse in v) {
158
167
  return v[keyToUse];
159
168
  }
@@ -224,37 +233,26 @@ function createFetch(optionsOrUrl: FetchOptions | string): MethodDecorator {
224
233
  return readBody(res);
225
234
  };
226
235
 
227
- // 호출 인자는 유지한 채, @fetchSettled 자리만 채운다
228
- const injectSettled = (inst: any, methodName: string | symbol, args: any[], settled: PromiseSettledResult<any>): any[] => {
229
- const saves = getParameterMetadata(inst, methodName);
230
- if (!saves.length) return args;
231
- const out = [...args];
232
- for (const s of saves) {
233
- if (s.kind === 'fetchSettled') out[s.index] = settled;
234
- }
235
- return out;
236
- };
236
+ const trigger = (options as { trigger?: string }).trigger;
237
+ const isAfter = trigger === 'after' || (trigger !== 'before' && 'valueKey' in options);
238
+ const manual = (options as FetchManual).manual;
237
239
 
238
- descriptor.value = async function (...args: any[]) {
239
- const inst = this as any;
240
+ /**
241
+ * 호출 한 번을 시작한다. 건너뛸 호출(떨어진 뒤 시작 / filter false)이면 null.
242
+ * 프레임워크가 끊는 경우(disconnect / abortPrevious)는 에러가 아니라 "할 일 없음" — frameworkAborted() 로 구분한다.
243
+ */
244
+ const begin = (inst: any, args: any[]) => {
240
245
  let helper: HelperHostSet | undefined;
241
246
  try {
242
247
  helper = SwcUtils.getHelperAndHostSet(inst);
243
248
  } catch {
244
249
  helper = undefined;
245
250
  }
246
-
247
- // disconnect 된 뒤에 새로 시작하는 호출(예: 떠나기 직전 걸어둔 setTimeout)도 조용히 건너뛴다.
251
+ // disconnect 된 뒤에 새로 시작하는 호출(예: 떠나기 직전 걸어둔 setTimeout)은 건너뛴다.
248
252
  // 한 번도 connect 안 된 요소는 해당 없음 (FetchLifeCycler 가 disconnect 때만 표시)
249
- if (inst?.__swc_fetchDetached && inst.isConnected === false) return undefined;
253
+ if (inst?.__swc_fetchDetached && inst.isConnected === false) return null;
254
+ if (options.filter && !options.filter({ self: inst, helper: helper!, args })) return null;
250
255
 
251
- // filter → before → (before 모드: fetch → 메서드 / after 모드: 메서드 → fetch) → finally
252
- if (options.filter && !(await options.filter(inst, helper!, args))) return undefined;
253
- if (options.before) await options.before(inst, helper!, args);
254
-
255
- const manual = (options as FetchManual).manual;
256
-
257
- // 프레임워크가 끊는 경우(disconnect / abortPrevious)는 에러가 아니라 "할 일 없음" → 조용히 undefined
258
256
  const controller = new AbortController();
259
257
  const active = getActiveControllers(inst);
260
258
  active.add(controller);
@@ -264,8 +262,12 @@ function createFetch(optionsOrUrl: FetchOptions | string): MethodDecorator {
264
262
  latest.set(propertyKey, controller);
265
263
  }
266
264
  const frameworkAborted = () => controller.signal.aborted;
265
+ const cleanup = () => {
266
+ active.delete(controller);
267
+ if (options.abortPrevious && getLatestControllers(inst).get(propertyKey) === controller) getLatestControllers(inst).delete(propertyKey);
268
+ };
267
269
 
268
- // 사용자 manual 이 signal 을 무시해도 abort 즉시 호출이 끝나도록 abort 와 경쟁시킨다
270
+ // 사용자 manual 이 signal 을 무시해도 abort 즉시 끝나도록 abort 와 경쟁시킨다
269
271
  const aborted = new Promise<never>((_, reject) => {
270
272
  if (controller.signal.aborted) reject(controller.signal.reason);
271
273
  else controller.signal.addEventListener('abort', () => reject(controller.signal.reason), { once: true });
@@ -273,80 +275,163 @@ function createFetch(optionsOrUrl: FetchOptions | string): MethodDecorator {
273
275
  aborted.catch(() => {}); // race 에 안 쓰이고 끝나도 unhandled rejection 이 안 나게
274
276
 
275
277
  // 수동형이면 manual(…, signal, returnValue), 선언형이면 url/init 으로 기본 fetch
276
- const runFetch = async (init?: RequestInit, returnValue?: any): Promise<PromiseSettledResult<any>> => {
277
- let settled: PromiseSettledResult<any>;
278
- try {
279
- const pending = Promise.resolve(manual
280
- ? manual(inst, helper!, args, controller.signal, returnValue)
281
- : defaultFetching(helper, resolveUrl(inst, helper!, args), { ...init, signal: combineSignals(init?.signal, controller.signal) }));
282
- pending.catch(() => {}); // abort 로 race 에서 진 뒤 늦게 reject 돼도 unhandled rejection 이 안 나게
283
- settled = { status: 'fulfilled', value: await Promise.race([pending, aborted]) };
284
- } catch (reason) {
285
- settled = { status: 'rejected', reason };
286
- }
287
- // 떨어진 요소의 DOM 을 건드리지 않게, 프레임워크 abort 면 after 훅은 건너뛴다
288
- if (options.after && !frameworkAborted()) await options.after(inst, helper!, args, settled);
289
- return settled;
278
+ const doFetch = (init?: RequestInit, returnValue?: any): Promise<any> => {
279
+ const pending = Promise.resolve().then(() => manual
280
+ ? manual(inst, helper!, args, controller.signal, returnValue)
281
+ : defaultFetching(helper, resolveUrl(inst, helper!, args), { ...init, signal: combineSignals(init?.signal, controller.signal) }));
282
+ pending.catch(() => {}); // abort 로 race 에서 진 뒤 늦게 reject 돼도 unhandled rejection 이 안 나게
283
+ return Promise.race([pending, aborted]);
290
284
  };
291
285
 
292
- let callArgs = args;
293
- let result: any;
294
- let error: any;
295
- try {
296
- const trigger = (options as { trigger?: string }).trigger;
297
- const isAfter = trigger === 'after' || (trigger !== 'before' && 'valueKey' in options);
298
- if (isAfter) {
299
- // after: 메서드 실행 → body 추출 → request 와 합쳐 fetch → fetch 결과 리턴.
300
- const returned = await originalMethod.apply(inst, callArgs);
301
- const body = extractBody(returned);
302
- let settled: PromiseSettledResult<any>;
303
- if (manual) {
304
- settled = await runFetch(undefined, body);
305
- } else {
306
- const reqOpt = (options as FetchDeclarative<ReturnRequestFactory>).request;
307
- const reqInit: RequestInit | undefined = typeof reqOpt === 'string'
308
- ? { method: reqOpt }
309
- : typeof reqOpt === 'function' ? await reqOpt(inst, helper!, args, returned) : undefined;
310
- settled = await runFetch({ ...reqInit, body: reqInit?.body ?? body });
311
- }
312
- if (frameworkAborted()) {
313
- error = controller.signal.reason;
314
- return undefined;
315
- }
316
- if (settled.status === 'rejected') throw settled.reason;
317
- result = settled.value;
318
- } else {
319
- // before: request 조립 → fetch → settled 를 주입해 메서드 실행. 메서드 결과 리턴.
320
- const reqOpt = manual ? undefined : (options as FetchDeclarative<RequestFactory>).request;
321
- const init: RequestInit | undefined = typeof reqOpt === 'string' ? { method: reqOpt } : typeof reqOpt === 'function' ? await reqOpt(inst, helper!, args) : undefined;
322
- const settled = await runFetch(init);
323
- if (frameworkAborted()) {
324
- // 떨어진 요소에 그리지 않게 메서드도 실행하지 않는다
325
- error = controller.signal.reason;
326
- return undefined;
327
- }
328
- callArgs = injectSettled(inst, propertyKey, args, settled);
329
- result = await originalMethod.apply(inst, callArgs);
330
- }
331
- return result;
332
- } catch (e) {
333
- error = e;
334
- throw e;
335
- } finally {
336
- active.delete(controller);
337
- if (options.abortPrevious && getLatestControllers(inst).get(propertyKey) === controller) getLatestControllers(inst).delete(propertyKey);
338
- // finally 는 정리용이라 프레임워크 abort 여도 실행 (ctx.error 에 AbortError)
339
- if (options.finally) await options.finally(inst, helper!, args, { args: callArgs, result, error });
286
+ // before: request 조립(인자 기반) → fetch
287
+ const fetchBefore = () => {
288
+ const reqOpt = manual ? undefined : (options as FetchDeclarative<RequestFactory>).request;
289
+ return Promise.resolve()
290
+ .then(() => (typeof reqOpt === 'string' ? { method: reqOpt } : typeof reqOpt === 'function' ? reqOpt(inst, helper!, args) : undefined))
291
+ .then(init => doFetch(init));
292
+ };
293
+ // after: returned(메서드 리턴 / 필드면 마지막 인자)에서 body 를 뽑아 request 와 합쳐 fetch
294
+ const fetchAfter = async (returned: any) => {
295
+ const body = extractBody(returned);
296
+ if (manual) return doFetch(undefined, body);
297
+ const reqOpt = (options as FetchDeclarative<ReturnRequestFactory>).request;
298
+ const reqInit: RequestInit | undefined = typeof reqOpt === 'string'
299
+ ? { method: reqOpt }
300
+ : typeof reqOpt === 'function' ? await reqOpt(inst, helper!, args, returned) : undefined;
301
+ return doFetch({ ...reqInit, body: reqInit?.body ?? body });
302
+ };
303
+ // 결과 Promise: 프레임워크가 끊으면 끝나지 않는다. 사용자가 안 받아도 unhandled rejection 이 안 나게
304
+ const quiet = (run: Promise<any>): Promise<any> => {
305
+ const promise = new Promise<any>((resolve, reject) => {
306
+ run.then(v => { if (!frameworkAborted()) resolve(v); }, e => { if (!frameworkAborted()) reject(e); }).finally(cleanup);
307
+ });
308
+ promise.catch(() => {});
309
+ return promise;
310
+ };
311
+
312
+ return { frameworkAborted, cleanup, fetchBefore, fetchAfter, quiet };
313
+ };
314
+
315
+ // ── 필드: declare 필드의 값을 프레임워크가 만든다 ──
316
+ // 선언이 함수(`(…) => Promise<T>`)든 값(`Promise<T>`)든 데코레이터가 받는 건 (prototype, 이름) 뿐이고
317
+ // 타입은 런타임에 없다(빌드 도구에 따라 design:type 도 없음). 그래서 필드 값은 "부를 수 있는 함수이면서 Promise" 하나로 만든다:
318
+ // - 부르면 (함수로 선언한 경우): 부를 때마다 새 요청. 인자 = url/request 재료, after(POST…)면 마지막 인자가 body
319
+ // - then/catch/finally/await (Promise 로 선언한 경우): 기억된 요청. 처음이면 요청, 성공하면 기억, 실패하면 잊어서 다음에 다시
320
+ // - 대입: undefined / null 이면 잊음, 다른 값/Promise 면 그걸 기억 (예: SSR 로 받은 값)
321
+ // 사용자는 선언한 타입대로만 쓴다 (TS 가 Promise 로 선언한 필드의 호출을 막는다).
322
+ if (isField) {
323
+ // 타입 정보가 있는 빌드(emitDecoratorMetadata)면 함수/Promise 가 아닌 선언을 바로 알린다 (없으면 컴파일 검사만)
324
+ const designType = ReflectUtils.getMetadata('design:type', target, propertyKey);
325
+ if (designType !== undefined && designType !== Function && designType !== Promise && designType !== Object) {
326
+ throw new Error(`[SWC] @fetch field "${String(propertyKey)}" must be a function returning a Promise (\`declare ${String(propertyKey)}: (…) => Promise<T>\`) or a Promise (\`declare ${String(propertyKey)}: Promise<T>\`), not ${designType?.name ?? String(designType)}.`);
327
+ }
328
+ // 같은 이름을 다른 데코레이터가 이미 차지했으면(접근자 등) 정의 시점에 바로 알린다
329
+ if (Object.prototype.hasOwnProperty.call(target, propertyKey)) {
330
+ throw new Error(`[SWC] "${String(propertyKey)}" is a @fetch field but something else already defines it. Put triggers/outputs on a method that calls this.${String(propertyKey)}().`);
331
+ }
332
+
333
+ // 기억된 요청 (Promise 로 쓸 때). body 는 request 팩토리로 (인자 없음 → args = [], returnValue = undefined)
334
+ const remembered = (inst: any): Promise<any> => {
335
+ const cache = cacheOf(inst);
336
+ const hit = cache.get(propertyKey);
337
+ if (hit) return hit;
338
+ const call = begin(inst, []);
339
+ if (!call) return never(); // 건너뛴 호출은 기억하지 않는다
340
+ const promise = call.quiet(isAfter ? call.fetchAfter(undefined) : call.fetchBefore());
341
+ cache.set(propertyKey, promise);
342
+ promise.catch(() => { if (cache.get(propertyKey) === promise) cache.delete(propertyKey); });
343
+ return promise;
344
+ };
345
+
346
+ // 인스턴스마다 필드 값(함수 겸 Promise) 하나 — 읽을 때마다 같은 값
347
+ const values = new WeakMap<object, any>();
348
+ const get = function (this: any) {
349
+ const inst = this;
350
+ let value = values.get(inst);
351
+ if (value) return value;
352
+ value = function (...args: any[]) {
353
+ const call = begin(inst, args);
354
+ if (!call) return never();
355
+ return call.quiet(isAfter ? call.fetchAfter(args[args.length - 1]) : call.fetchBefore());
356
+ };
357
+ value.then = (ok?: any, fail?: any) => remembered(inst).then(ok, fail);
358
+ value.catch = (fail?: any) => remembered(inst).catch(fail);
359
+ value.finally = (done?: any) => remembered(inst).finally(done);
360
+ // instanceof Promise 로 Promise 를 알아보는 코드(출력 데코레이터 등)도 Promise 로 보게 한다.
361
+ // then/catch/finally 는 위에서 직접 달았으니 Promise.prototype 의 것(내부 슬롯 필요)은 쓰이지 않는다.
362
+ Object.setPrototypeOf(value, Promise.prototype);
363
+ values.set(inst, value);
364
+ return value;
365
+ };
366
+ (get as any)[FETCH_FIELD_FN] = true;
367
+ Object.defineProperty(target, propertyKey, {
368
+ get,
369
+ set(this: any, v: any) {
370
+ const cache = cacheOf(this);
371
+ if (v === undefined || v === null) cache.delete(propertyKey);
372
+ else cache.set(propertyKey, Promise.resolve(v));
373
+ },
374
+ configurable: true,
375
+ });
376
+ return;
377
+ }
378
+
379
+ // ── 메서드: 트리거 핸들러용 ──
380
+ const originalMethod = descriptor!.value;
381
+
382
+ // after 모드는 fetch 결과를 리턴하므로 래퍼가 항상 Promise 를 돌려준다 — 동기 메서드면 선언과 어긋나니 알려준다
383
+ let warnedSync = false;
384
+ const isPromiseLike = (v: any) => v instanceof Promise || (v && typeof v.then === 'function');
385
+ const guardSync = (raw: any) => {
386
+ if (!warnedSync && originalMethod?.constructor?.name !== 'AsyncFunction' && !isPromiseLike(raw)) {
387
+ warnedSync = true;
388
+ console.warn(`[SWC] @fetch (after) on "${String(propertyKey)}" always returns a Promise: declare the method async (or with an explicit Promise return type) so the signature matches.`);
340
389
  }
341
390
  };
342
391
 
392
+ // 호출 인자는 유지한 채, @fetchPromise 자리만 채운다
393
+ const injectPromise = (inst: any, methodName: string | symbol, args: any[], promise: Promise<any>): any[] => {
394
+ const saves = getParameterMetadata(inst, methodName);
395
+ if (!saves.length) return args;
396
+ const out = [...args];
397
+ for (const s of saves) {
398
+ if (s.kind === 'fetchPromise') out[s.index] = promise;
399
+ }
400
+ return out;
401
+ };
402
+
403
+ descriptor!.value = function (...args: any[]) {
404
+ const inst = this as any;
405
+ const call = begin(inst, args);
406
+ if (!call) return undefined;
407
+
408
+ if (!isAfter) {
409
+ // before: 요청을 시작하고, 그 Promise 를 @fetchPromise 자리에 넣어 메서드를 바로 부른다. 메서드 반환값을 그대로 리턴.
410
+ return originalMethod.apply(inst, injectPromise(inst, propertyKey, args, call.quiet(call.fetchBefore())));
411
+ }
412
+
413
+ // after: 메서드 실행 → 리턴값에서 body → fetch → fetch 결과 리턴
414
+ return (async () => {
415
+ try {
416
+ const raw = originalMethod.apply(inst, args);
417
+ guardSync(raw);
418
+ return await call.fetchAfter(await raw);
419
+ } catch (e) {
420
+ if (call.frameworkAborted()) return undefined; // 끊긴 호출은 조용히
421
+ throw e;
422
+ } finally {
423
+ call.cleanup();
424
+ }
425
+ })();
426
+ };
427
+
343
428
  return descriptor;
344
- };
429
+ }) as FetchDecorator;
345
430
  }
346
431
 
347
- export function fetch(url: string): MethodDecorator;
348
- export function fetch(options: FetchOptions): MethodDecorator;
349
- export function fetch(optionsOrUrl: FetchOptions | string): MethodDecorator {
432
+ export function fetch(url: string): FetchDecorator;
433
+ export function fetch(options: FetchOptions): FetchDecorator;
434
+ export function fetch(optionsOrUrl: FetchOptions | string): FetchDecorator {
350
435
  return createFetch(optionsOrUrl);
351
436
  }
352
437
 
@@ -365,7 +450,7 @@ export type FetchManualOptions = FetchType & FetchTriggerOptions;
365
450
  * @fetchManual(fn, options?) = @fetch({ manual: fn, ...options })
366
451
  * 서비스 호출 등 요청을 직접 만들 때. 예: @fetchManual((self) => self.myService.getProfile({}))
367
452
  */
368
- export function fetchManual(manual: FetchManualFn, options?: FetchManualOptions): MethodDecorator {
453
+ export function fetchManual(manual: FetchManualFn, options?: FetchManualOptions): FetchDecorator {
369
454
  return createFetch({ ...(options ?? {}), manual } as FetchOptions);
370
455
  }
371
456
 
@@ -376,7 +461,7 @@ export type FetchAfterMethodOptions = FetchType & Omit<FetchDeclarative<ReturnRe
376
461
 
377
462
  type FetchUrl = RequestInfo | URL | FetchUrlFactory;
378
463
 
379
- const beforeMethod = (method: string) => (url: FetchUrl, options?: FetchBeforeMethodOptions): MethodDecorator => {
464
+ const beforeMethod = (method: string) => (url: FetchUrl, options?: FetchBeforeMethodOptions): FetchDecorator => {
380
465
  const { request, ...rest } = options ?? {};
381
466
  return createFetch({
382
467
  ...rest,
@@ -386,7 +471,7 @@ const beforeMethod = (method: string) => (url: FetchUrl, options?: FetchBeforeMe
386
471
  } as FetchOptions);
387
472
  };
388
473
 
389
- const afterMethod = (method: string) => (url: FetchUrl, options?: FetchAfterMethodOptions): MethodDecorator => {
474
+ const afterMethod = (method: string) => (url: FetchUrl, options?: FetchAfterMethodOptions): FetchDecorator => {
390
475
  const { request, ...rest } = options ?? {};
391
476
  return createFetch({
392
477
  ...rest,
@@ -396,7 +481,7 @@ const afterMethod = (method: string) => (url: FetchUrl, options?: FetchAfterMeth
396
481
  } as FetchOptions);
397
482
  };
398
483
 
399
- /** @fetchGet(url, options?) — before 모드 GET. 결과는 @fetchSettled 으로 */
484
+ /** @fetchGet(url, options?) — before 모드 GET. 요청 Promise 는 @fetchPromise 로 */
400
485
  export const fetchGet = beforeMethod('GET');
401
486
  /** @fetchDelete(url, options?) — before 모드 DELETE (body 없음) */
402
487
  export const fetchDelete = beforeMethod('DELETE');
@@ -411,20 +496,20 @@ export const fetchPatch = afterMethod('PATCH');
411
496
  * @fetchLatest(url | options) = @fetch({ ...options, abortPrevious: true })
412
497
  * 연달아 부르면 이전 요청을 끊고 마지막 것만 살린다 (검색 자동완성 등)
413
498
  */
414
- export function fetchLatest(url: string): MethodDecorator;
415
- export function fetchLatest(options: FetchOptions): MethodDecorator;
416
- export function fetchLatest(optionsOrUrl: FetchOptions | string): MethodDecorator {
499
+ export function fetchLatest(url: string): FetchDecorator;
500
+ export function fetchLatest(options: FetchOptions): FetchDecorator;
501
+ export function fetchLatest(optionsOrUrl: FetchOptions | string): FetchDecorator {
417
502
  const options: FetchOptions = typeof optionsOrUrl === 'string' ? { url: optionsOrUrl } : optionsOrUrl;
418
503
  return createFetch({ ...options, abortPrevious: true } as FetchOptions);
419
504
  }
420
505
 
421
506
  /** @fetchBefore(options) = @fetch({ ...options, trigger: 'before' }) */
422
- export function fetchBefore(options: FetchType & (FetchDeclarative<RequestFactory> | FetchManual)): MethodDecorator {
507
+ export function fetchBefore(options: FetchType & (FetchDeclarative<RequestFactory> | FetchManual)): FetchDecorator {
423
508
  return createFetch({ ...options, trigger: 'before' } as FetchOptions);
424
509
  }
425
510
 
426
511
  /** @fetchAfter(options) = @fetch({ ...options, trigger: 'after' }) */
427
- export function fetchAfter(options: FetchType & { valueKey?: string } & (FetchDeclarative<ReturnRequestFactory> | FetchManual)): MethodDecorator {
512
+ export function fetchAfter(options: FetchType & { valueKey?: string } & (FetchDeclarative<ReturnRequestFactory> | FetchManual)): FetchDecorator {
428
513
  return createFetch({ ...options, trigger: 'after' } as FetchOptions);
429
514
  }
430
515
 
@@ -434,18 +519,63 @@ export const getFetchMetadata = (target: any): FetchMetadata[] | undefined => {
434
519
  return ReflectUtils.findAllMetadata<any[]>(FETCH_METADATA_KEY, constructor).flat();
435
520
  };
436
521
 
522
+ // 메타데이터 값 안 어딘가가 이 이름(propertyKey)을 가리키는지 (Map 키 / 배열 / { propertyKey })
523
+ const refersTo = (v: any, key: string | symbol): boolean => {
524
+ if (v instanceof Map) return v.has(key) || [...v.values()].some(x => refersTo(x, key));
525
+ if (Array.isArray(v)) return v.some(x => refersTo(x, key));
526
+ return !!v && typeof v === 'object' && v.propertyKey === key;
527
+ };
528
+ const findDescriptor = (proto: any, key: string | symbol): PropertyDescriptor | undefined => {
529
+ for (let p = proto; p && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
530
+ const d = Object.getOwnPropertyDescriptor(p, key);
531
+ if (d) return d;
532
+ }
533
+ return undefined;
534
+ };
535
+ const fieldHint = (key: string | symbol) => `Put triggers/outputs on a method that calls this.${String(key)}().`;
536
+
537
+ /**
538
+ * @elementDefine 이 클래스 정의 시점에 부른다 (멤버 데코레이터가 다 붙은 뒤라 붙인 순서와 무관).
539
+ * 필드 @fetch 와 같은 이름에 다른 SWC 데코레이터가 붙었으면 에러 — 메서드 데코레이터는 TS 가 이미 막지만
540
+ * JS / `as any` 로 우회한 경우, 필드용 데코레이터(@attribute, @query, @property …)까지 잡는다.
541
+ */
542
+ export const assertFetchFields = (constructor: any): void => {
543
+ const fields = (getFetchMetadata(constructor) ?? []).filter(m => m.field).map(m => m.propertyKey);
544
+ for (const key of fields) {
545
+ const d = findDescriptor(constructor.prototype, key);
546
+ if (!d?.value?.[FETCH_FIELD_FN] && !(d?.get as any)?.[FETCH_FIELD_FN]) {
547
+ throw new Error(`[SWC] "${String(key)}" is a @fetch field but another decorator replaced it. ${fieldHint(key)}`);
548
+ }
549
+ for (const mk of ReflectUtils.getMetadataKeys(constructor)) {
550
+ if (mk === FETCH_METADATA_KEY || typeof mk === 'string') continue; // design:* 등 TS 메타데이터는 제외
551
+ if (refersTo(ReflectUtils.getMetadata(mk, constructor), key)) {
552
+ const name = typeof mk === 'symbol' ? mk.description?.replace('simple-web-component:', '@') : String(mk);
553
+ throw new Error(`[SWC] "${String(key)}" is a @fetch field — it can't also have ${name}. ${fieldHint(key)}`);
554
+ }
555
+ }
556
+ }
557
+ };
558
+
437
559
  /**
438
560
  * disconnect 시 진행 중인 @fetch 요청을 전부 abort (메서드/after 는 실행되지 않고 조용히 끝난다).
439
561
  * 떨어진 동안 새로 시작하는 호출도 건너뛰도록 표시하고, 다시 connect 되면 표시를 지운다.
440
562
  */
441
563
  export class FetchLifeCycler implements ElementDefineLifeCycler {
442
564
  onConnected(helperHostSet: HelperHostSet): void {
443
- helperHostSet.$this.__swc_fetchDetached = false;
565
+ const inst = helperHostSet.$this;
566
+ inst.__swc_fetchDetached = false;
567
+ // declare 를 빠뜨리면 인스턴스 필드(undefined)가 프로토타입의 요청 함수를 가린다
568
+ for (const m of getFetchMetadata(inst) ?? []) {
569
+ if (m.field && Object.prototype.hasOwnProperty.call(inst, m.propertyKey)) {
570
+ throw new Error(`[SWC] @fetch field "${String(m.propertyKey)}" must be declared with \`declare\` (e.g. \`declare ${String(m.propertyKey)}: (…) => Promise<T>\`) — a plain field shadows the request function.`);
571
+ }
572
+ }
444
573
  }
445
574
 
446
575
  onDisconnected(helperHostSet: HelperHostSet): void {
447
576
  const inst = helperHostSet.$this;
448
577
  inst.__swc_fetchDetached = true;
578
+ valueCache.delete(inst); // 끊긴 요청은 끝나지 않으니, 다시 붙은 뒤 읽으면 새로 요청하도록 기억을 지운다
449
579
  const active: Set<AbortController> | undefined = inst.__swc_fetchControllers;
450
580
  if (!active?.size) return;
451
581
  for (const c of active) c.abort(abortError('element disconnected'));
@@ -26,3 +26,4 @@ export * from './eventMedia';
26
26
  export * from './parameter';
27
27
  export * from './fetch';
28
28
  export * from './around';
29
+ export * from './persistState';
@@ -1,6 +1,6 @@
1
1
  import { ReflectUtils } from '@dooboostore/core';
2
2
  import { SpecialSelector, SwcQueryOptions, SwcFnSelector, SwcSelector, HelperHostSet } from '../types';
3
- import { buildSwcParameterArgs } from './parameter';
3
+ import { buildSwcParameterArgs, buildCommonKindValues } from './parameter';
4
4
 
5
5
  // 공통 옵션 — IntersectionObserverInit + filter/removeObserver. root·delegate 제외.
6
6
  // 주의: 데코레이터의 root(light/shadow/all/auto) 는 셀렉터 스코프 선택용이다.
@@ -205,11 +205,10 @@ export class IntersectionObserverLifeCycler implements ElementDefineLifeCycler {
205
205
  void (async () => {
206
206
  const helper = SwcUtils.getHelperAndHostSet(inst);
207
207
  if (opts.filter && !(await opts.filter(matchedEls, { currentThis: inst, helper }))) return;
208
- const hostSet = SwcUtils.getHostSet(inst);
209
- const helperSet = SwcUtils.getHelperSet(helperHostSet.$w);
210
- const legacyArgs = [matchedEls, entries, obs, { ...hostSet, $root: root }];
208
+ const common = buildCommonKindValues(inst, helperHostSet.$w);
209
+ const legacyArgs = [matchedEls, entries, obs, { ...common.hostSet, $root: root }];
211
210
  const buildArgs = (beforeReturn: any) => buildSwcParameterArgs(inst, m.propertyKey, {
212
- hostSet, helperHostSet: helper, helperSet, intersectionObserverBeforeReturn: beforeReturn
211
+ ...common, intersectionObserverBeforeReturn: beforeReturn
213
212
  }, [...legacyArgs, beforeReturn]);
214
213
  let args = buildArgs(undefined);
215
214
  if (opts.before) { const br = await opts.before(matchedEls, { currentThis: inst, helper }); args = buildArgs(br); }
@@ -1,6 +1,6 @@
1
1
  import { ReflectUtils } from '@dooboostore/core';
2
2
  import { SpecialSelector, SwcQueryOptions, SwcSelector, SwcFnSelector, HelperHostSet } from '../types';
3
- import { buildSwcParameterArgs } from './parameter';
3
+ import { buildSwcParameterArgs, buildCommonKindValues } from './parameter';
4
4
 
5
5
  // 공통 옵션 — root·delegate 없음 (MutationObserverInit + filter/removeObserver)
6
6
  export interface MutationObserverBaseOptions extends MutationObserverInit {
@@ -263,11 +263,10 @@ export class MutationObserverLifeCycler implements ElementDefineLifeCycler {
263
263
  void (async () => {
264
264
  const helper = SwcUtils.getHelperAndHostSet(inst);
265
265
  if (options.filter && !(await options.filter(matchedEls, { currentThis: inst, helper }))) return;
266
- const hostSet = SwcUtils.getHostSet(inst);
267
- const helperSet = SwcUtils.getHelperSet(helperHostSet.$w);
268
- const legacyArgs = [matchedEls, records, obs, { ...hostSet, $root: root }];
266
+ const common = buildCommonKindValues(inst, helperHostSet.$w);
267
+ const legacyArgs = [matchedEls, records, obs, { ...common.hostSet, $root: root }];
269
268
  const buildArgs = (beforeReturn: any) => buildSwcParameterArgs(inst, meta.propertyKey, {
270
- hostSet, helperHostSet: helper, helperSet, mutationObserverBeforeReturn: beforeReturn
269
+ ...common, mutationObserverBeforeReturn: beforeReturn
271
270
  }, [...legacyArgs, beforeReturn]);
272
271
  let args = buildArgs(undefined);
273
272
  if (options.before) { const br = await options.before(matchedEls, { currentThis: inst, helper }); args = buildArgs(br); }