@dooboostore/simple-web-component 1.0.49 → 1.0.50

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 (145) hide show
  1. package/README.md +72 -0
  2. package/dist/cjs/decorators/addEventListener.js +1994 -9
  3. package/dist/cjs/decorators/addEventListener.js.map +2 -2
  4. package/dist/cjs/decorators/applyAttribute.js +10 -4
  5. package/dist/cjs/decorators/applyAttribute.js.map +2 -2
  6. package/dist/cjs/decorators/applyClass.js +10 -4
  7. package/dist/cjs/decorators/applyClass.js.map +2 -2
  8. package/dist/cjs/decorators/applyNode.js +10 -4
  9. package/dist/cjs/decorators/applyNode.js.map +2 -2
  10. package/dist/cjs/decorators/applyProperty.js +14 -8
  11. package/dist/cjs/decorators/applyProperty.js.map +2 -2
  12. package/dist/cjs/decorators/applySlot.js +6 -5
  13. package/dist/cjs/decorators/applySlot.js.map +2 -2
  14. package/dist/cjs/decorators/applyStyle.js +10 -4
  15. package/dist/cjs/decorators/applyStyle.js.map +2 -2
  16. package/dist/cjs/decorators/changedAttribute.js +22 -13
  17. package/dist/cjs/decorators/changedAttribute.js.map +2 -2
  18. package/dist/cjs/decorators/elementDefine.js +18 -3
  19. package/dist/cjs/decorators/elementDefine.js.map +2 -2
  20. package/dist/cjs/decorators/eventMedia.js +84 -0
  21. package/dist/cjs/decorators/eventMedia.js.map +7 -0
  22. package/dist/cjs/decorators/index.js +3 -0
  23. package/dist/cjs/decorators/index.js.map +2 -2
  24. package/dist/cjs/decorators/intersectionObserver.js +46 -55
  25. package/dist/cjs/decorators/intersectionObserver.js.map +2 -2
  26. package/dist/cjs/decorators/mutationObserver.js +46 -55
  27. package/dist/cjs/decorators/mutationObserver.js.map +2 -2
  28. package/dist/cjs/decorators/parameter.js +73 -0
  29. package/dist/cjs/decorators/parameter.js.map +7 -0
  30. package/dist/cjs/decorators/requestAnimationFrame.js +124 -0
  31. package/dist/cjs/decorators/requestAnimationFrame.js.map +7 -0
  32. package/dist/cjs/decorators/resizeObserver.js +46 -55
  33. package/dist/cjs/decorators/resizeObserver.js.map +2 -2
  34. package/dist/cjs/decorators/setInterval.js +46 -14
  35. package/dist/cjs/decorators/setInterval.js.map +2 -2
  36. package/dist/cjs/decorators/setTimeout.js +49 -14
  37. package/dist/cjs/decorators/setTimeout.js.map +2 -2
  38. package/dist/cjs/elements/SwcAppMixin.js +19 -3
  39. package/dist/cjs/elements/SwcAppMixin.js.map +2 -2
  40. package/dist/cjs/types/window.d.js.map +1 -1
  41. package/dist/esm/decorators/addEventListener.js +1994 -9
  42. package/dist/esm/decorators/addEventListener.js.map +2 -2
  43. package/dist/esm/decorators/applyAttribute.js +10 -4
  44. package/dist/esm/decorators/applyAttribute.js.map +2 -2
  45. package/dist/esm/decorators/applyClass.js +10 -4
  46. package/dist/esm/decorators/applyClass.js.map +2 -2
  47. package/dist/esm/decorators/applyNode.js +10 -4
  48. package/dist/esm/decorators/applyNode.js.map +2 -2
  49. package/dist/esm/decorators/applyProperty.js +14 -8
  50. package/dist/esm/decorators/applyProperty.js.map +2 -2
  51. package/dist/esm/decorators/applySlot.js +6 -5
  52. package/dist/esm/decorators/applySlot.js.map +2 -2
  53. package/dist/esm/decorators/applyStyle.js +10 -4
  54. package/dist/esm/decorators/applyStyle.js.map +2 -2
  55. package/dist/esm/decorators/changedAttribute.js +22 -13
  56. package/dist/esm/decorators/changedAttribute.js.map +2 -2
  57. package/dist/esm/decorators/elementDefine.js +18 -3
  58. package/dist/esm/decorators/elementDefine.js.map +2 -2
  59. package/dist/esm/decorators/eventMedia.js +66 -0
  60. package/dist/esm/decorators/eventMedia.js.map +7 -0
  61. package/dist/esm/decorators/index.js +3 -0
  62. package/dist/esm/decorators/index.js.map +2 -2
  63. package/dist/esm/decorators/intersectionObserver.js +46 -55
  64. package/dist/esm/decorators/intersectionObserver.js.map +2 -2
  65. package/dist/esm/decorators/mutationObserver.js +46 -55
  66. package/dist/esm/decorators/mutationObserver.js.map +2 -2
  67. package/dist/esm/decorators/parameter.js +54 -0
  68. package/dist/esm/decorators/parameter.js.map +7 -0
  69. package/dist/esm/decorators/requestAnimationFrame.js +105 -0
  70. package/dist/esm/decorators/requestAnimationFrame.js.map +7 -0
  71. package/dist/esm/decorators/resizeObserver.js +46 -55
  72. package/dist/esm/decorators/resizeObserver.js.map +2 -2
  73. package/dist/esm/decorators/setInterval.js +46 -15
  74. package/dist/esm/decorators/setInterval.js.map +2 -2
  75. package/dist/esm/decorators/setTimeout.js +49 -15
  76. package/dist/esm/decorators/setTimeout.js.map +2 -2
  77. package/dist/esm/elements/SwcAppMixin.js +20 -4
  78. package/dist/esm/elements/SwcAppMixin.js.map +2 -2
  79. package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +2813 -571
  80. package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
  81. package/dist/types/decorators/addEventListener.d.ts +962 -0
  82. package/dist/types/decorators/addEventListener.d.ts.map +1 -1
  83. package/dist/types/decorators/applyAttribute.d.ts +2 -0
  84. package/dist/types/decorators/applyAttribute.d.ts.map +1 -1
  85. package/dist/types/decorators/applyClass.d.ts +2 -0
  86. package/dist/types/decorators/applyClass.d.ts.map +1 -1
  87. package/dist/types/decorators/applyNode.d.ts +2 -0
  88. package/dist/types/decorators/applyNode.d.ts.map +1 -1
  89. package/dist/types/decorators/applyProperty.d.ts +4 -0
  90. package/dist/types/decorators/applyProperty.d.ts.map +1 -1
  91. package/dist/types/decorators/applySlot.d.ts.map +1 -1
  92. package/dist/types/decorators/applyStyle.d.ts +2 -0
  93. package/dist/types/decorators/applyStyle.d.ts.map +1 -1
  94. package/dist/types/decorators/changedAttribute.d.ts +1 -3
  95. package/dist/types/decorators/changedAttribute.d.ts.map +1 -1
  96. package/dist/types/decorators/elementDefine.d.ts.map +1 -1
  97. package/dist/types/decorators/eventMedia.d.ts +25 -0
  98. package/dist/types/decorators/eventMedia.d.ts.map +1 -0
  99. package/dist/types/decorators/index.d.ts +3 -0
  100. package/dist/types/decorators/index.d.ts.map +1 -1
  101. package/dist/types/decorators/intersectionObserver.d.ts +9 -0
  102. package/dist/types/decorators/intersectionObserver.d.ts.map +1 -1
  103. package/dist/types/decorators/mutationObserver.d.ts +10 -1
  104. package/dist/types/decorators/mutationObserver.d.ts.map +1 -1
  105. package/dist/types/decorators/parameter.d.ts +35 -0
  106. package/dist/types/decorators/parameter.d.ts.map +1 -0
  107. package/dist/types/decorators/requestAnimationFrame.d.ts +47 -0
  108. package/dist/types/decorators/requestAnimationFrame.d.ts.map +1 -0
  109. package/dist/types/decorators/resizeObserver.d.ts +9 -0
  110. package/dist/types/decorators/resizeObserver.d.ts.map +1 -1
  111. package/dist/types/decorators/setInterval.d.ts +17 -5
  112. package/dist/types/decorators/setInterval.d.ts.map +1 -1
  113. package/dist/types/decorators/setTimeout.d.ts +17 -5
  114. package/dist/types/decorators/setTimeout.d.ts.map +1 -1
  115. package/dist/types/elements/SwcAppMixin.d.ts.map +1 -1
  116. package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +2813 -571
  117. package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
  118. package/package.json +3 -3
  119. package/src/decorators/addEventListener.ts +1060 -8
  120. package/src/decorators/applyAttribute.ts +14 -4
  121. package/src/decorators/applyClass.ts +14 -4
  122. package/src/decorators/applyNode.ts +14 -4
  123. package/src/decorators/applyProperty.ts +28 -8
  124. package/src/decorators/applySlot.ts +6 -5
  125. package/src/decorators/applyStyle.ts +14 -4
  126. package/src/decorators/changedAttribute.ts +30 -15
  127. package/src/decorators/elementDefine.ts +32 -5
  128. package/src/decorators/eventMedia.ts +78 -0
  129. package/src/decorators/index.ts +3 -0
  130. package/src/decorators/intersectionObserver.ts +77 -61
  131. package/src/decorators/mutationObserver.ts +78 -61
  132. package/src/decorators/parameter.ts +81 -0
  133. package/src/decorators/requestAnimationFrame.ts +158 -0
  134. package/src/decorators/resizeObserver.ts +76 -60
  135. package/src/decorators/setInterval.ts +71 -15
  136. package/src/decorators/setTimeout.ts +74 -15
  137. package/src/elements/SwcAppMixin.ts +20 -5
  138. package/src/types/window.d.ts +2 -18
  139. package/dist/cjs/examples/RxJSOperatorsExample.js +0 -285
  140. package/dist/cjs/examples/RxJSOperatorsExample.js.map +0 -7
  141. package/dist/esm/examples/RxJSOperatorsExample.js +0 -266
  142. package/dist/esm/examples/RxJSOperatorsExample.js.map +0 -7
  143. package/dist/types/examples/RxJSOperatorsExample.d.ts +0 -94
  144. package/dist/types/examples/RxJSOperatorsExample.d.ts.map +0 -1
  145. package/src/examples/RxJSOperatorsExample.ts +0 -279
@@ -0,0 +1,158 @@
1
+ import { ReflectUtils } from '@dooboostore/core';
2
+ import { ensureInit, getElementConfig } from './elementDefine';
3
+ import { SwcUtils } from '../utils/Utils';
4
+ import { ElementDefineLifeCycler, HelperHostSet } from '../types';
5
+
6
+ export const REQUEST_ANIMATION_FRAME_METADATA_KEY = Symbol.for('simple-web-component:request-animation-frame');
7
+
8
+ export type RequestAnimationFrameType = 'onConnected' | 'returnValue';
9
+
10
+ export interface RequestAnimationFrameOptions {
11
+ /**
12
+ * 'onConnected' - connect 시 자동으로 루프 시작 (매 프레임 데코레이트된 메서드를 직접 호출), disconnect 시 자동 정리.
13
+ * 'returnValue'(기본값) - 자동 시작 없음. 개발자가 이 메서드를 직접 호출해야 시작되고,
14
+ * 그 호출의 리턴값(또는 valueKey로 뽑은 값)이 실제로 매 프레임 호출될 함수가 된다.
15
+ */
16
+ type?: RequestAnimationFrameType;
17
+ /** 루프 시작 직후 1회 호출 (디버깅/로깅용). id는 첫 requestAnimationFrame 반환값 - cleanup은 자동으로 처리되므로 직접 취소할 필요 없음. */
18
+ created?: (set: HelperHostSet, id: number) => void;
19
+ /**
20
+ * type:'returnValue' 전용 - 리턴값이 함수면 그 자체를, 객체면 이 키의 값이 함수일 때 그 값을
21
+ * "매 프레임 호출될 함수"로 사용한다. 안 주면 REQUEST_ANIMATION_FRAME_METADATA_KEY로 폴백한다 -
22
+ * applyAttribute.ts/applyNode.ts의 valueKey와 동일한 컨벤션.
23
+ */
24
+ valueKey?: symbol | string;
25
+ }
26
+
27
+ export interface RequestAnimationFrameMetadata {
28
+ propertyKey: string | symbol;
29
+ options: RequestAnimationFrameOptions;
30
+ }
31
+
32
+ /**
33
+ * 프레임 콜백 시그니처.
34
+ * - timestamp: 이번 프레임의 시각 (requestAnimationFrame이 넘겨주는 값)
35
+ * - prevValue: 직전 호출이 리턴한 값 (첫 호출에서는 undefined)
36
+ * 리턴값이 undefined/null이면 다음 프레임을 예약하지 않고 루프가 멈춘다 (개발자가 종료를 제어).
37
+ * undefined/null이 아닌 값을 리턴하면 그 값 그대로 다음 호출의 prevValue로 전달된다.
38
+ */
39
+ export type FrameCallback = (timestamp: number, prevValue?: any) => any;
40
+
41
+ const activeMap = new WeakMap<any, number[]>();
42
+ const getActiveEntries = (inst: any): number[] => {
43
+ let entries = activeMap.get(inst);
44
+ if (!entries) {
45
+ entries = [];
46
+ activeMap.set(inst, entries);
47
+ }
48
+ return entries;
49
+ };
50
+
51
+ const startFrameLoop = (win: Window, entries: number[], frameFn: FrameCallback): number => {
52
+ let currentId = 0;
53
+ const step = (timestamp: number, prevValue?: any) => {
54
+ const idx = entries.indexOf(currentId);
55
+ if (idx >= 0) entries.splice(idx, 1);
56
+ let result: any;
57
+ try {
58
+ result = frameFn(timestamp, prevValue);
59
+ } catch (e) {
60
+ console.error('[SWC] requestAnimationFrame callback error:', e);
61
+ return;
62
+ }
63
+ if (result === undefined || result === null) return; // 개발자가 종료를 신호함 - 다음 프레임 예약 안 함
64
+ currentId = win.requestAnimationFrame(ts => step(ts, result));
65
+ entries.push(currentId);
66
+ };
67
+ currentId = win.requestAnimationFrame(ts => step(ts, undefined));
68
+ entries.push(currentId);
69
+ return currentId;
70
+ };
71
+
72
+ const applyRequestAnimationFrame = (options: RequestAnimationFrameOptions, target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): PropertyDescriptor | void => {
73
+ const constructor = target.constructor;
74
+ const normalizedOptions: RequestAnimationFrameOptions = { ...options, type: options.type ?? 'returnValue' };
75
+
76
+ let metaList = ReflectUtils.getOwnMetadata(REQUEST_ANIMATION_FRAME_METADATA_KEY, constructor) as RequestAnimationFrameMetadata[];
77
+ if (!metaList) {
78
+ metaList = [];
79
+ ReflectUtils.defineMetadata(REQUEST_ANIMATION_FRAME_METADATA_KEY, metaList, constructor);
80
+ }
81
+ metaList.push({ propertyKey, options: normalizedOptions });
82
+
83
+ if (normalizedOptions.type === 'onConnected' || !descriptor) {
84
+ return; // LifeCycler가 connect/disconnect를 전담 - wrapping 불필요
85
+ }
86
+
87
+ const original = descriptor.value;
88
+ descriptor.value = function (this: any, ...args: any[]) {
89
+ const res = original.apply(this, args);
90
+ // valueKey를 안 주면 이 데코레이터 전용 METADATA_KEY로 폴백 (applyAttribute.ts/applyNode.ts와 동일한 컨벤션)
91
+ const vk = normalizedOptions.valueKey ?? REQUEST_ANIMATION_FRAME_METADATA_KEY;
92
+ const frameFn: FrameCallback | undefined =
93
+ typeof res === 'function' ? res : res && typeof res === 'object' && typeof res[vk] === 'function' ? res[vk] : undefined;
94
+
95
+ if (frameFn) {
96
+ ensureInit(this);
97
+ const conf = getElementConfig(this);
98
+ const win = (this as any)._resolveWindow?.(conf) || ((typeof window !== 'undefined' ? window : undefined) as Window);
99
+ const id = startFrameLoop(win, getActiveEntries(this), frameFn);
100
+ normalizedOptions.created?.(SwcUtils.getHelperAndHostSet(win, this), id);
101
+ }
102
+ return res;
103
+ };
104
+ return descriptor;
105
+ };
106
+
107
+ /**
108
+ * type:'onConnected' - connect 시 자동으로 매 프레임 루프를 시작하고, disconnect 시 자동으로 cancelAnimationFrame 한다.
109
+ * type:'returnValue'(기본값) - 메서드를 감싸지 않는 대신, 호출될 때마다(개발자가 직접 호출) 원본 로직을
110
+ * 그대로 실행하고 리턴값(또는 valueKey로 뽑은 값)이 함수면 그 함수를 매 프레임 호출되는 루프로 등록한다.
111
+ * 원본 리턴값은 그대로 통과시키므로 다른 데코레이터와 같은 메서드에 스택해도 순서에 상관없이 안전하다.
112
+ *
113
+ * 옵션 없이 `@requestAnimationFrame` 그대로 붙여도 되고, `@requestAnimationFrame({...})`처럼 옵션을 줄 수도 있다.
114
+ */
115
+ export function requestAnimationFrame(options?: RequestAnimationFrameOptions): MethodDecorator;
116
+ export function requestAnimationFrame(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
117
+ export function requestAnimationFrame(optionsOrTarget?: RequestAnimationFrameOptions | Object, propertyKey?: string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
118
+ if (propertyKey !== undefined) {
119
+ // 옵션 없이: @requestAnimationFrame
120
+ applyRequestAnimationFrame({}, optionsOrTarget as Object, propertyKey, descriptor);
121
+ return;
122
+ }
123
+ // 옵션과 함께: @requestAnimationFrame() 또는 @requestAnimationFrame({...})
124
+ return (target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor) => {
125
+ return applyRequestAnimationFrame((optionsOrTarget as RequestAnimationFrameOptions) ?? {}, target, propertyKey, descriptor);
126
+ };
127
+ }
128
+
129
+ export const findAllRequestAnimationFrameMetadata = (target: any): RequestAnimationFrameMetadata[] => {
130
+ const constructor = target instanceof Function ? target : target.constructor;
131
+ return (ReflectUtils.findAllMetadata<RequestAnimationFrameMetadata[]>(REQUEST_ANIMATION_FRAME_METADATA_KEY, constructor) || []).flat();
132
+ };
133
+
134
+ export class RequestAnimationFrameLifeCycler implements ElementDefineLifeCycler {
135
+ onConnected(helperHostSet: HelperHostSet): void {
136
+ const inst = helperHostSet.$this;
137
+ activeMap.delete(inst); // 재연결 대비 리셋
138
+ const entries = getActiveEntries(inst);
139
+ for (const meta of findAllRequestAnimationFrameMetadata(inst)) {
140
+ if (meta.options.type !== 'onConnected') continue; // returnValue 타입은 수동 호출을 기다림
141
+ const frameFn: FrameCallback = (timestamp, prevValue) => inst[meta.propertyKey](timestamp, prevValue);
142
+ const id = startFrameLoop(helperHostSet.$w, entries, frameFn);
143
+ meta.options.created?.(helperHostSet, id);
144
+ }
145
+ }
146
+
147
+ onDisconnected(helperHostSet: HelperHostSet): void {
148
+ const inst = helperHostSet.$this;
149
+ for (const id of activeMap.get(inst) ?? []) {
150
+ try {
151
+ helperHostSet.$w.cancelAnimationFrame(id);
152
+ } catch (e) {
153
+ console.error('[SWC] cancelAnimationFrame error:', e);
154
+ }
155
+ }
156
+ activeMap.delete(inst);
157
+ }
158
+ }
@@ -26,107 +26,123 @@ export interface ResizeObserverMetadata {
26
26
 
27
27
  export const RESIZE_OBSERVER_METADATA_KEY = Symbol.for('simple-web-component:resize-observer');
28
28
 
29
+ const applyResizeObserver = (selector: SwcSelector, options: ResizeObserverOptions, targetObj: Object, propertyKey: string | symbol): void => {
30
+ const constructor = targetObj.constructor;
31
+ let observers = ReflectUtils.getMetadata<ResizeObserverMetadata[]>(RESIZE_OBSERVER_METADATA_KEY, constructor);
32
+ if (!observers) {
33
+ observers = [];
34
+ ReflectUtils.defineMetadata(RESIZE_OBSERVER_METADATA_KEY, observers, constructor);
35
+ }
36
+ observers.push({ propertyKey, selector, options });
37
+ };
38
+
39
+ const resolveResizeObserverArgs = (
40
+ selectorOrOptions: SwcSelector | ResizeObserverOptions | undefined,
41
+ maybeOptions: ResizeObserverOptions | undefined,
42
+ extra: Partial<ResizeObserverOptions>
43
+ ): { selector: SwcSelector; options: ResizeObserverOptions } => {
44
+ if (typeof selectorOrOptions === 'string' || typeof selectorOrOptions === 'function') {
45
+ return { selector: selectorOrOptions, options: { ...(maybeOptions ?? {}), ...extra } };
46
+ }
47
+ return { selector: '$this', options: { ...(selectorOrOptions ?? {}), ...extra } };
48
+ };
49
+
50
+ /**
51
+ * bare(@decorator)와 factory(@decorator()/@decorator(...)) 호출을 모두 처리하는 공통 디스패처.
52
+ * extra는 Light/Shadow/All/Delegate 변형이 강제로 덧씌우는 옵션(root, delegate 등)이다.
53
+ */
54
+ const dispatchResizeObserver = (
55
+ selectorOrOptionsOrTarget: SwcSelector | ResizeObserverOptions | Object | undefined,
56
+ maybeOptionsOrPropertyKey: ResizeObserverOptions | string | symbol | undefined,
57
+ descriptor: PropertyDescriptor | undefined,
58
+ extra: Partial<ResizeObserverOptions>
59
+ ): MethodDecorator | void => {
60
+ if ((typeof maybeOptionsOrPropertyKey === 'string' || typeof maybeOptionsOrPropertyKey === 'symbol') && descriptor !== undefined) {
61
+ // 옵션 없이: @decorator
62
+ applyResizeObserver('$this', { ...extra }, selectorOrOptionsOrTarget as Object, maybeOptionsOrPropertyKey);
63
+ return;
64
+ }
65
+ // 옵션과 함께: @decorator() / @decorator(selector, options) / @decorator(options)
66
+ const { selector, options } = resolveResizeObserverArgs(
67
+ selectorOrOptionsOrTarget as SwcSelector | ResizeObserverOptions | undefined,
68
+ maybeOptionsOrPropertyKey as ResizeObserverOptions | undefined,
69
+ extra
70
+ );
71
+ return (targetObj: Object, propertyKey: string | symbol) => {
72
+ applyResizeObserver(selector, options, targetObj, propertyKey);
73
+ };
74
+ };
75
+
29
76
  export function resizeObserver(target: SpecialSelector, options?: ResizeObserverQueryOptions): MethodDecorator;
30
77
  export function resizeObserver(selector: string, options?: ResizeObserverQueryOptions): MethodDecorator;
31
78
  export function resizeObserver(selector: SwcFnSelector, options?: ResizeObserverNonQueryOptions): MethodDecorator;
32
79
  export function resizeObserver(options?: ResizeObserverQueryOptions): MethodDecorator;
80
+ export function resizeObserver(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
33
81
  /**
34
82
  * @resizeObserver decorator to observe element size changes.
83
+ * 옵션 없이 `@resizeObserver` 그대로 붙여도 되고, `@resizeObserver(...)`처럼 셀렉터/옵션을 줄 수도 있다.
35
84
  */
36
- export function resizeObserver(selectorOrOptions?: SwcSelector | ResizeObserverOptions, maybeOptions?: ResizeObserverOptions): MethodDecorator {
37
- return (targetObj: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
38
- let selector: SwcSelector = '$this';
39
- let options: ResizeObserverOptions = {};
40
-
41
- if (typeof selectorOrOptions === 'string' || typeof selectorOrOptions === 'function') {
42
- selector = selectorOrOptions;
43
- options = maybeOptions ?? {};
44
- } else {
45
- selector = '$this';
46
- options = selectorOrOptions ?? {};
47
- }
48
-
49
- const constructor = targetObj.constructor;
50
-
51
- let observers = ReflectUtils.getMetadata<ResizeObserverMetadata[]>(RESIZE_OBSERVER_METADATA_KEY, constructor);
52
- if (!observers) {
53
- observers = [];
54
- ReflectUtils.defineMetadata(RESIZE_OBSERVER_METADATA_KEY, observers, constructor);
55
- }
56
-
57
- observers.push({ propertyKey, selector, options });
58
- };
85
+ export function resizeObserver(selectorOrOptionsOrTarget?: SwcSelector | ResizeObserverOptions | Object, maybeOptionsOrPropertyKey?: ResizeObserverOptions | string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
86
+ return dispatchResizeObserver(selectorOrOptionsOrTarget, maybeOptionsOrPropertyKey, descriptor, {});
59
87
  }
60
88
 
61
- export function resizeObserverThis(options?: ResizeObserverQueryOptions): MethodDecorator {
62
- return resizeObserver('$this', options);
89
+ export function resizeObserverThis(options?: ResizeObserverQueryOptions): MethodDecorator;
90
+ export function resizeObserverThis(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
91
+ export function resizeObserverThis(optionsOrTarget?: ResizeObserverQueryOptions | Object, propertyKey?: string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
92
+ return dispatchResizeObserver(optionsOrTarget, propertyKey, descriptor, {});
63
93
  }
64
94
 
65
95
  // ─── root별 delegate 헬퍼 (root 주입 → 문자열 셀렉터 전용) ───
66
96
 
67
97
  export function resizeObserverDelegateLight(selector: string, options?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator;
68
98
  export function resizeObserverDelegateLight(options?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator;
69
- export function resizeObserverDelegateLight(selectorOrOptions?: string | Omit<ResizeObserverQueryOptions, 'delegate'>, maybeOptions?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator {
70
- if (typeof selectorOrOptions === 'string' || typeof selectorOrOptions === 'function') {
71
- return resizeObserver(selectorOrOptions as any, {...maybeOptions ?? {}, root: 'light', delegate: true});
72
- }
73
- return resizeObserver({...selectorOrOptions ?? {}, root: 'light', delegate: true});
99
+ export function resizeObserverDelegateLight(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
100
+ export function resizeObserverDelegateLight(selectorOrOptionsOrTarget?: string | Omit<ResizeObserverQueryOptions, 'delegate'> | Object, maybeOptionsOrPropertyKey?: Omit<ResizeObserverQueryOptions, 'delegate'> | string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
101
+ return dispatchResizeObserver(selectorOrOptionsOrTarget as any, maybeOptionsOrPropertyKey as any, descriptor, { root: 'light', delegate: true });
74
102
  }
75
103
 
76
104
  export function resizeObserverDelegateShadow(selector: string, options?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator;
77
105
  export function resizeObserverDelegateShadow(options?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator;
78
- export function resizeObserverDelegateShadow(selectorOrOptions?: string | Omit<ResizeObserverQueryOptions, 'delegate'>, maybeOptions?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator {
79
- if (typeof selectorOrOptions === 'string' || typeof selectorOrOptions === 'function') {
80
- return resizeObserver(selectorOrOptions as any, {...maybeOptions ?? {}, root: 'shadow', delegate: true});
81
- }
82
- return resizeObserver({...selectorOrOptions ?? {}, root: 'shadow', delegate: true});
106
+ export function resizeObserverDelegateShadow(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
107
+ export function resizeObserverDelegateShadow(selectorOrOptionsOrTarget?: string | Omit<ResizeObserverQueryOptions, 'delegate'> | Object, maybeOptionsOrPropertyKey?: Omit<ResizeObserverQueryOptions, 'delegate'> | string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
108
+ return dispatchResizeObserver(selectorOrOptionsOrTarget as any, maybeOptionsOrPropertyKey as any, descriptor, { root: 'shadow', delegate: true });
83
109
  }
84
110
 
85
111
  export function resizeObserverDelegateAll(selector: string, options?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator;
86
112
  export function resizeObserverDelegateAll(options?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator;
87
- export function resizeObserverDelegateAll(selectorOrOptions?: string | Omit<ResizeObserverQueryOptions, 'delegate'>, maybeOptions?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator {
88
- if (typeof selectorOrOptions === 'string' || typeof selectorOrOptions === 'function') {
89
- return resizeObserver(selectorOrOptions as any, {...maybeOptions ?? {}, root: 'all', delegate: true});
90
- }
91
- return resizeObserver({...selectorOrOptions ?? {}, root: 'all', delegate: true});
113
+ export function resizeObserverDelegateAll(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
114
+ export function resizeObserverDelegateAll(selectorOrOptionsOrTarget?: string | Omit<ResizeObserverQueryOptions, 'delegate'> | Object, maybeOptionsOrPropertyKey?: Omit<ResizeObserverQueryOptions, 'delegate'> | string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
115
+ return dispatchResizeObserver(selectorOrOptionsOrTarget as any, maybeOptionsOrPropertyKey as any, descriptor, { root: 'all', delegate: true });
92
116
  }
93
117
 
94
118
  export function resizeObserverDelegate(selector: string, options?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator;
95
119
  export function resizeObserverDelegate(options?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator;
96
- export function resizeObserverDelegate(selectorOrOptions?: string | Omit<ResizeObserverQueryOptions, 'delegate'>, maybeOptions?: Omit<ResizeObserverQueryOptions, 'delegate'>): MethodDecorator {
97
- if (typeof selectorOrOptions === 'string' || typeof selectorOrOptions === 'function') {
98
- return resizeObserver(selectorOrOptions as any, {...maybeOptions ?? {}, root: 'auto', delegate: true});
99
- }
100
- return resizeObserver({...selectorOrOptions ?? {}, root: 'auto', delegate: true});
120
+ export function resizeObserverDelegate(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
121
+ export function resizeObserverDelegate(selectorOrOptionsOrTarget?: string | Omit<ResizeObserverQueryOptions, 'delegate'> | Object, maybeOptionsOrPropertyKey?: Omit<ResizeObserverQueryOptions, 'delegate'> | string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
122
+ return dispatchResizeObserver(selectorOrOptionsOrTarget as any, maybeOptionsOrPropertyKey as any, descriptor, { root: 'auto', delegate: true });
101
123
  }
102
124
 
103
125
  // ─── root별 일반 헬퍼 (delegate 없이, 셀렉터 생략 시 $this) ───
104
126
 
105
127
  export function resizeObserverLight(selector: string, options?: ResizeObserverQueryOptions): MethodDecorator;
106
128
  export function resizeObserverLight(options?: ResizeObserverQueryOptions): MethodDecorator;
107
- export function resizeObserverLight(selectorOrOptions?: string | ResizeObserverQueryOptions, maybeOptions?: ResizeObserverQueryOptions): MethodDecorator {
108
- if (typeof selectorOrOptions === 'string' || typeof selectorOrOptions === 'function') {
109
- return resizeObserver(selectorOrOptions as any, {...maybeOptions ?? {}, root: 'light'});
110
- }
111
- return resizeObserver({...selectorOrOptions ?? {}, root: 'light'});
129
+ export function resizeObserverLight(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
130
+ export function resizeObserverLight(selectorOrOptionsOrTarget?: string | ResizeObserverQueryOptions | Object, maybeOptionsOrPropertyKey?: ResizeObserverQueryOptions | string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
131
+ return dispatchResizeObserver(selectorOrOptionsOrTarget as any, maybeOptionsOrPropertyKey as any, descriptor, { root: 'light' });
112
132
  }
113
133
 
114
134
  export function resizeObserverShadow(selector: string, options?: ResizeObserverQueryOptions): MethodDecorator;
115
135
  export function resizeObserverShadow(options?: ResizeObserverQueryOptions): MethodDecorator;
116
- export function resizeObserverShadow(selectorOrOptions?: string | ResizeObserverQueryOptions, maybeOptions?: ResizeObserverQueryOptions): MethodDecorator {
117
- if (typeof selectorOrOptions === 'string' || typeof selectorOrOptions === 'function') {
118
- return resizeObserver(selectorOrOptions as any, {...maybeOptions ?? {}, root: 'shadow'});
119
- }
120
- return resizeObserver({...selectorOrOptions ?? {}, root: 'shadow'});
136
+ export function resizeObserverShadow(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
137
+ export function resizeObserverShadow(selectorOrOptionsOrTarget?: string | ResizeObserverQueryOptions | Object, maybeOptionsOrPropertyKey?: ResizeObserverQueryOptions | string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
138
+ return dispatchResizeObserver(selectorOrOptionsOrTarget as any, maybeOptionsOrPropertyKey as any, descriptor, { root: 'shadow' });
121
139
  }
122
140
 
123
141
  export function resizeObserverAll(selector: string, options?: ResizeObserverQueryOptions): MethodDecorator;
124
142
  export function resizeObserverAll(options?: ResizeObserverQueryOptions): MethodDecorator;
125
- export function resizeObserverAll(selectorOrOptions?: string | ResizeObserverQueryOptions, maybeOptions?: ResizeObserverQueryOptions): MethodDecorator {
126
- if (typeof selectorOrOptions === 'string' || typeof selectorOrOptions === 'function') {
127
- return resizeObserver(selectorOrOptions as any, {...maybeOptions ?? {}, root: 'all'});
128
- }
129
- return resizeObserver({...selectorOrOptions ?? {}, root: 'all'});
143
+ export function resizeObserverAll(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
144
+ export function resizeObserverAll(selectorOrOptionsOrTarget?: string | ResizeObserverQueryOptions | Object, maybeOptionsOrPropertyKey?: ResizeObserverQueryOptions | string | symbol, descriptor?: PropertyDescriptor): MethodDecorator | void {
145
+ return dispatchResizeObserver(selectorOrOptionsOrTarget as any, maybeOptionsOrPropertyKey as any, descriptor, { root: 'all' });
130
146
  }
131
147
 
132
148
  export function getResizeObserverMetadata(target: any): ResizeObserverMetadata[] | undefined {
@@ -1,16 +1,28 @@
1
1
  import { ReflectUtils } from '@dooboostore/core';
2
+ import { ensureInit, getElementConfig } from './elementDefine';
3
+ import { SwcUtils } from '../utils/Utils';
2
4
  import { ElementDefineLifeCycler, HelperHostSet } from '../types';
3
5
 
4
6
  export const SET_INTERVAL_METADATA_KEY = Symbol.for('simple-web-component:set-interval');
5
7
 
8
+ export type SetIntervalType = 'onConnected' | 'returnValue';
9
+
6
10
  export interface SetIntervalOptions {
7
- /** 매 tick마다 데코레이트된 메서드에 넘길 인자를 계산. 없으면 인자 없이 호출. */
11
+ /**
12
+ * 'onConnected' (connect 시 자동으로 시작, disconnect 시 자동 정리) |
13
+ * 'returnValue' (기본값 - 자동 시작 없음. 개발자가 이 메서드를 직접 호출해야 시작되고,
14
+ * 그 호출의 리턴값이 실제로 반복 실행될 함수가 된다 - 아래 설명 참고).
15
+ */
16
+ type?: SetIntervalType;
17
+ /** 매 tick마다 데코레이트된 메서드에 넘길 인자를 계산. type:'onConnected'에서만 의미가 있음 (없으면 인자 없이 호출). */
8
18
  parameter?: (set: HelperHostSet) => any[];
9
19
  /** 타이머 등록 직후 1회 호출 (디버깅/로깅용). id는 실제 setInterval 반환값 - cleanup은 자동으로 처리되므로 직접 clear할 필요 없음. */
10
20
  created?: (set: HelperHostSet, id: number) => void;
11
21
  /**
12
- * 매 tick마다 데코레이트된 메서드의 리턴값이 객체이고 이 키의 값이 함수이면 (id: number)로 호출한다.
13
- * applyAttribute.ts의 valueKey와 동일한 컨벤션 - 리턴 객체에서 이 데코레이터 몫만 뽑아 쓴다.
22
+ * type:'onConnected' - 매 tick마다 데코레이트된 메서드의 리턴값이 객체이고 이 키의 값이 함수이면 (id: number)로 호출한다.
23
+ * type:'returnValue' - 리턴값이 함수면 그 자체를, 객체면 이 키의 값이 함수일 때 그 값을 "실제로 반복 실행될 함수"로 사용한다.
24
+ * 안 주면 SET_INTERVAL_METADATA_KEY로 폴백한다 - applyAttribute.ts/applyNode.ts의 valueKey와 동일한 컨벤션
25
+ * (리턴 객체에서 이 데코레이터 몫만 뽑아 쓰고, 다른 데코레이터와 스택할 때만 커스텀 키로 구분하면 된다).
14
26
  */
15
27
  valueKey?: symbol | string;
16
28
  }
@@ -21,18 +33,64 @@ export interface SetIntervalMetadata {
21
33
  options: SetIntervalOptions;
22
34
  }
23
35
 
36
+ const activeMap = new WeakMap<any, number[]>();
37
+ const getActiveEntries = (inst: any): number[] => {
38
+ let entries = activeMap.get(inst);
39
+ if (!entries) {
40
+ entries = [];
41
+ activeMap.set(inst, entries);
42
+ }
43
+ return entries;
44
+ };
45
+
24
46
  /**
25
- * connect 시 자동으로 setInterval을 걸고, disconnect 시 자동으로 clearInterval 한다.
47
+ * type:'onConnected' - connect 시 자동으로 setInterval을 걸고, disconnect 시 자동으로 clearInterval 한다.
48
+ * type:'returnValue'(기본값) - 메서드를 감싸지 않는 대신, 호출될 때마다(개발자가 직접 호출) 원본 로직을
49
+ * 그대로 실행하고 리턴값(또는 valueKey로 뽑은 값)이 함수면 그 함수를 새 setInterval의 반복 콜백으로 등록한다.
50
+ * 호출할 때마다 별도의 새 타이머가 생기며, disconnect 시 전부 정리된다. 원본 리턴값은 그대로 통과시키므로
51
+ * 다른 데코레이터와 같은 메서드에 스택해도 순서에 상관없이 안전하다.
26
52
  */
27
53
  export function setInterval(interval: number, options: SetIntervalOptions = {}): MethodDecorator {
28
- return (target: Object, propertyKey: string | symbol) => {
54
+ return (target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor) => {
29
55
  const constructor = target.constructor;
56
+ const normalizedOptions: SetIntervalOptions = { ...options, type: options.type ?? 'returnValue' };
57
+
30
58
  let metaList = ReflectUtils.getOwnMetadata(SET_INTERVAL_METADATA_KEY, constructor) as SetIntervalMetadata[];
31
59
  if (!metaList) {
32
60
  metaList = [];
33
61
  ReflectUtils.defineMetadata(SET_INTERVAL_METADATA_KEY, metaList, constructor);
34
62
  }
35
- metaList.push({ propertyKey, interval, options });
63
+ metaList.push({ propertyKey, interval, options: normalizedOptions });
64
+
65
+ if (normalizedOptions.type === 'onConnected' || !descriptor) {
66
+ return; // LifeCycler가 connect/disconnect를 전담 - wrapping 불필요
67
+ }
68
+
69
+ const original = descriptor.value;
70
+ descriptor.value = function (this: any, ...args: any[]) {
71
+ const res = original.apply(this, args);
72
+ // valueKey를 안 주면 이 데코레이터 전용 METADATA_KEY로 폴백 (applyAttribute.ts/applyNode.ts와 동일한 컨벤션)
73
+ const vk = normalizedOptions.valueKey ?? SET_INTERVAL_METADATA_KEY;
74
+ const tickFn: ((id: number) => void) | undefined =
75
+ typeof res === 'function' ? res : res && typeof res === 'object' && typeof res[vk] === 'function' ? res[vk] : undefined;
76
+
77
+ if (tickFn) {
78
+ ensureInit(this);
79
+ const conf = getElementConfig(this);
80
+ const win = (this as any)._resolveWindow?.(conf) || ((typeof window !== 'undefined' ? window : undefined) as Window);
81
+ const id = win.setInterval(() => {
82
+ try {
83
+ tickFn(id);
84
+ } catch (e) {
85
+ console.error('[SWC] setInterval tick error:', e);
86
+ }
87
+ }, interval);
88
+ normalizedOptions.created?.(SwcUtils.getHelperAndHostSet(win, this), id);
89
+ getActiveEntries(this).push(id);
90
+ }
91
+ return res;
92
+ };
93
+ return descriptor;
36
94
  };
37
95
  }
38
96
 
@@ -42,18 +100,17 @@ export const findAllSetIntervalMetadata = (target: any): SetIntervalMetadata[] =
42
100
  };
43
101
 
44
102
  export class SetIntervalLifeCycler implements ElementDefineLifeCycler {
45
- private readonly activeMap = new WeakMap<any, number[]>();
46
-
47
103
  onConnected(helperHostSet: HelperHostSet): void {
48
104
  const inst = helperHostSet.$this;
49
- const ids: number[] = [];
105
+ activeMap.delete(inst); // 재연결 대비 리셋
50
106
  for (const meta of findAllSetIntervalMetadata(inst)) {
107
+ if (meta.options.type !== 'onConnected') continue; // returnValue 타입은 수동 호출을 기다림
51
108
  const id = helperHostSet.$w.setInterval(() => {
52
109
  const args = meta.options.parameter?.(helperHostSet) ?? [];
53
110
  try {
54
111
  const res = inst[meta.propertyKey](...args);
55
- const vk = meta.options.valueKey;
56
- if (vk && res && typeof res === 'object' && typeof res[vk] === 'function') {
112
+ const vk = meta.options.valueKey ?? SET_INTERVAL_METADATA_KEY;
113
+ if (res && typeof res === 'object' && typeof res[vk] === 'function') {
57
114
  res[vk](id);
58
115
  }
59
116
  } catch (e) {
@@ -61,20 +118,19 @@ export class SetIntervalLifeCycler implements ElementDefineLifeCycler {
61
118
  }
62
119
  }, meta.interval);
63
120
  meta.options.created?.(helperHostSet, id);
64
- ids.push(id);
121
+ getActiveEntries(inst).push(id);
65
122
  }
66
- this.activeMap.set(inst, ids);
67
123
  }
68
124
 
69
125
  onDisconnected(helperHostSet: HelperHostSet): void {
70
126
  const inst = helperHostSet.$this;
71
- for (const id of this.activeMap.get(inst) ?? []) {
127
+ for (const id of activeMap.get(inst) ?? []) {
72
128
  try {
73
129
  helperHostSet.$w.clearInterval(id);
74
130
  } catch (e) {
75
131
  console.error('[SWC] clearInterval error:', e);
76
132
  }
77
133
  }
78
- this.activeMap.delete(inst);
134
+ activeMap.delete(inst);
79
135
  }
80
136
  }
@@ -1,16 +1,28 @@
1
1
  import { ReflectUtils } from '@dooboostore/core';
2
+ import { ensureInit, getElementConfig } from './elementDefine';
3
+ import { SwcUtils } from '../utils/Utils';
2
4
  import { ElementDefineLifeCycler, HelperHostSet } from '../types';
3
5
 
4
6
  export const SET_TIMEOUT_METADATA_KEY = Symbol.for('simple-web-component:set-timeout');
5
7
 
8
+ export type SetTimeoutType = 'onConnected' | 'returnValue';
9
+
6
10
  export interface SetTimeoutOptions {
7
- /** 실행 시 데코레이트된 메서드에 넘길 인자를 계산. 없으면 인자 없이 호출. */
11
+ /**
12
+ * 'onConnected' (connect 시 자동으로 예약, disconnect 시 자동 정리) |
13
+ * 'returnValue' (기본값 - 자동 예약 없음. 개발자가 이 메서드를 직접 호출해야 예약되고,
14
+ * 그 호출의 리턴값이 실제로 실행될 함수가 된다 - 아래 설명 참고).
15
+ */
16
+ type?: SetTimeoutType;
17
+ /** 실행 시 데코레이트된 메서드에 넘길 인자를 계산. type:'onConnected'에서만 의미가 있음 (없으면 인자 없이 호출). */
8
18
  parameter?: (set: HelperHostSet) => any[];
9
19
  /** 타이머 등록 직후 1회 호출 (디버깅/로깅용). id는 실제 setTimeout 반환값 - cleanup은 자동으로 처리되므로 직접 clear할 필요 없음. */
10
20
  created?: (set: HelperHostSet, id: number) => void;
11
21
  /**
12
- * fire 시 데코레이트된 메서드의 리턴값이 객체이고 이 키의 값이 함수이면 (id: number)로 호출한다.
13
- * applyAttribute.ts의 valueKey와 동일한 컨벤션 - 리턴 객체에서 이 데코레이터 몫만 뽑아 쓴다.
22
+ * type:'onConnected' - fire 시 데코레이트된 메서드의 리턴값이 객체이고 이 키의 값이 함수이면 (id: number)로 호출한다.
23
+ * type:'returnValue' - 리턴값이 함수면 그 자체를, 객체면 이 키의 값이 함수일 때 그 값을 "실제로 실행될 함수"로 사용한다.
24
+ * 안 주면 SET_TIMEOUT_METADATA_KEY로 폴백한다 - applyAttribute.ts/applyNode.ts의 valueKey와 동일한 컨벤션
25
+ * (리턴 객체에서 이 데코레이터 몫만 뽑아 쓰고, 다른 데코레이터와 스택할 때만 커스텀 키로 구분하면 된다).
14
26
  */
15
27
  valueKey?: symbol | string;
16
28
  }
@@ -21,18 +33,67 @@ export interface SetTimeoutMetadata {
21
33
  options: SetTimeoutOptions;
22
34
  }
23
35
 
36
+ const activeMap = new WeakMap<any, number[]>();
37
+ const getActiveEntries = (inst: any): number[] => {
38
+ let entries = activeMap.get(inst);
39
+ if (!entries) {
40
+ entries = [];
41
+ activeMap.set(inst, entries);
42
+ }
43
+ return entries;
44
+ };
45
+
24
46
  /**
25
- * connect 시 자동으로 setTimeout을 걸고, disconnect 시 (아직 실행 전이라면) 자동으로 clearTimeout 한다.
47
+ * type:'onConnected' - connect 시 자동으로 setTimeout을 걸고, disconnect 시 (아직 실행 전이라면) 자동으로 clearTimeout 한다.
48
+ * type:'returnValue'(기본값) - 메서드를 감싸지 않는 대신, 호출될 때마다(개발자가 직접 호출) 원본 로직을
49
+ * 그대로 실행하고 리턴값(또는 valueKey로 뽑은 값)이 함수면 그 함수를 새 setTimeout으로 예약한다.
50
+ * 호출할 때마다 별도의 새 타이머가 생기며, disconnect 시 (아직 실행 전인 것들은) 전부 정리된다.
51
+ * 원본 리턴값은 그대로 통과시키므로 다른 데코레이터와 같은 메서드에 스택해도 순서에 상관없이 안전하다.
26
52
  */
27
53
  export function setTimeout(delay: number, options: SetTimeoutOptions = {}): MethodDecorator {
28
- return (target: Object, propertyKey: string | symbol) => {
54
+ return (target: Object, propertyKey: string | symbol, descriptor?: PropertyDescriptor) => {
29
55
  const constructor = target.constructor;
56
+ const normalizedOptions: SetTimeoutOptions = { ...options, type: options.type ?? 'returnValue' };
57
+
30
58
  let metaList = ReflectUtils.getOwnMetadata(SET_TIMEOUT_METADATA_KEY, constructor) as SetTimeoutMetadata[];
31
59
  if (!metaList) {
32
60
  metaList = [];
33
61
  ReflectUtils.defineMetadata(SET_TIMEOUT_METADATA_KEY, metaList, constructor);
34
62
  }
35
- metaList.push({ propertyKey, delay, options });
63
+ metaList.push({ propertyKey, delay, options: normalizedOptions });
64
+
65
+ if (normalizedOptions.type === 'onConnected' || !descriptor) {
66
+ return; // LifeCycler가 connect/disconnect를 전담 - wrapping 불필요
67
+ }
68
+
69
+ const original = descriptor.value;
70
+ descriptor.value = function (this: any, ...args: any[]) {
71
+ const res = original.apply(this, args);
72
+ // valueKey를 안 주면 이 데코레이터 전용 METADATA_KEY로 폴백 (applyAttribute.ts/applyNode.ts와 동일한 컨벤션)
73
+ const vk = normalizedOptions.valueKey ?? SET_TIMEOUT_METADATA_KEY;
74
+ const tickFn: ((id: number) => void) | undefined =
75
+ typeof res === 'function' ? res : res && typeof res === 'object' && typeof res[vk] === 'function' ? res[vk] : undefined;
76
+
77
+ if (tickFn) {
78
+ ensureInit(this);
79
+ const conf = getElementConfig(this);
80
+ const win = (this as any)._resolveWindow?.(conf) || ((typeof window !== 'undefined' ? window : undefined) as Window);
81
+ const id = win.setTimeout(() => {
82
+ const entries = getActiveEntries(this);
83
+ const idx = entries.indexOf(id);
84
+ if (idx >= 0) entries.splice(idx, 1);
85
+ try {
86
+ tickFn(id);
87
+ } catch (e) {
88
+ console.error('[SWC] setTimeout fire error:', e);
89
+ }
90
+ }, delay);
91
+ normalizedOptions.created?.(SwcUtils.getHelperAndHostSet(win, this), id);
92
+ getActiveEntries(this).push(id);
93
+ }
94
+ return res;
95
+ };
96
+ return descriptor;
36
97
  };
37
98
  }
38
99
 
@@ -42,18 +103,17 @@ export const findAllSetTimeoutMetadata = (target: any): SetTimeoutMetadata[] =>
42
103
  };
43
104
 
44
105
  export class SetTimeoutLifeCycler implements ElementDefineLifeCycler {
45
- private readonly activeMap = new WeakMap<any, number[]>();
46
-
47
106
  onConnected(helperHostSet: HelperHostSet): void {
48
107
  const inst = helperHostSet.$this;
49
- const ids: number[] = [];
108
+ activeMap.delete(inst); // 재연결 대비 리셋
50
109
  for (const meta of findAllSetTimeoutMetadata(inst)) {
110
+ if (meta.options.type !== 'onConnected') continue; // returnValue 타입은 수동 호출을 기다림
51
111
  const id = helperHostSet.$w.setTimeout(() => {
52
112
  const args = meta.options.parameter?.(helperHostSet) ?? [];
53
113
  try {
54
114
  const res = inst[meta.propertyKey](...args);
55
- const vk = meta.options.valueKey;
56
- if (vk && res && typeof res === 'object' && typeof res[vk] === 'function') {
115
+ const vk = meta.options.valueKey ?? SET_TIMEOUT_METADATA_KEY;
116
+ if (res && typeof res === 'object' && typeof res[vk] === 'function') {
57
117
  res[vk](id);
58
118
  }
59
119
  } catch (e) {
@@ -61,20 +121,19 @@ export class SetTimeoutLifeCycler implements ElementDefineLifeCycler {
61
121
  }
62
122
  }, meta.delay);
63
123
  meta.options.created?.(helperHostSet, id);
64
- ids.push(id);
124
+ getActiveEntries(inst).push(id);
65
125
  }
66
- this.activeMap.set(inst, ids);
67
126
  }
68
127
 
69
128
  onDisconnected(helperHostSet: HelperHostSet): void {
70
129
  const inst = helperHostSet.$this;
71
- for (const id of this.activeMap.get(inst) ?? []) {
130
+ for (const id of activeMap.get(inst) ?? []) {
72
131
  try {
73
132
  helperHostSet.$w.clearTimeout(id);
74
133
  } catch (e) {
75
134
  console.error('[SWC] clearTimeout error:', e);
76
135
  }
77
136
  }
78
- this.activeMap.delete(inst);
137
+ activeMap.delete(inst);
79
138
  }
80
139
  }