@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
@@ -0,0 +1,408 @@
1
+ import { ReflectUtils } from '@dooboostore/core';
2
+
3
+ export type PersistStorage = 'local' | 'session' | 'cookie' | 'indexeddb';
4
+ /** Sync backends complete synchronously; async ones resolve in a microtask/macrotask. */
5
+ export const ASYNC_STORAGES: ReadonlySet<string> = new Set(['indexeddb']);
6
+ export const isAsyncStorage = (storage?: string) => ASYNC_STORAGES.has(storage ?? 'local');
7
+ export interface CookieOpts {
8
+ path?: string;
9
+ domain?: string;
10
+ maxAge?: number;
11
+ expires?: Date;
12
+ secure?: boolean;
13
+ sameSite?: 'Strict' | 'Lax' | 'None';
14
+ }
15
+ export interface CookieRemoveOpts {
16
+ path?: string;
17
+ domain?: string;
18
+ }
19
+ export type PersistOptions =
20
+ | { storage?: 'local' | 'session' | 'indexeddb'; valueKey?: string | symbol; serialize?: (v: any) => string; deserialize?: (s: string) => any }
21
+ | ({ storage: 'cookie'; valueKey?: string | symbol; serialize?: (v: any) => string; deserialize?: (s: string) => any } & CookieOpts);
22
+ export type RemovePersistOptions =
23
+ | { storage?: 'local' | 'session' | 'indexeddb'; valueKey?: string | symbol }
24
+ | ({ storage: 'cookie'; valueKey?: string | symbol } & CookieRemoveOpts);
25
+
26
+ /** Marks a method already wrapped for persist injection — prevents double-wrap. */
27
+ const PERSIST_WRAPPED = Symbol.for('simple-web-component:persist-wrapped');
28
+
29
+ const winOf = (inst: any): any => {
30
+ try {
31
+ return inst?.ownerDocument?.defaultView ?? (globalThis as any);
32
+ } catch { return globalThis as any; }
33
+ };
34
+ const storeOf = (w: any, storage: PersistStorage): Storage | null => {
35
+ try {
36
+ if (storage === 'local') return w?.localStorage ?? null;
37
+ if (storage === 'session') return w?.sessionStorage ?? null;
38
+ return null;
39
+ } catch { return null; }
40
+ };
41
+
42
+ const readCookie = (w: any, key: string): string | null => {
43
+ try {
44
+ const doc = w?.document ?? (globalThis as any)?.document;
45
+ if (!doc?.cookie && doc?.cookie !== '') return null;
46
+ const parts = String(doc.cookie).split(';');
47
+ for (const p of parts) {
48
+ const i = p.indexOf('=');
49
+ const k = p.slice(0, i).trim();
50
+ if (k === key) return decodeURIComponent(p.slice(i + 1).trim());
51
+ }
52
+ return null;
53
+ } catch { return null; }
54
+ };
55
+ const writeCookie = (w: any, key: string, raw: string, opts: CookieOpts = {}) => {
56
+ const doc = w?.document ?? (globalThis as any)?.document;
57
+ if (!doc) return;
58
+ let c = `${key}=${encodeURIComponent(raw)}; path=${opts.path ?? '/'}`;
59
+ if (opts.domain) c += `; domain=${opts.domain}`;
60
+ if (opts.maxAge !== undefined) c += `; max-age=${opts.maxAge}`;
61
+ if (opts.expires) c += `; expires=${opts.expires.toUTCString()}`;
62
+ if (opts.secure) c += '; secure';
63
+ if (opts.sameSite) c += `; samesite=${opts.sameSite}`;
64
+ try { doc.cookie = c; } catch { /* ignore */ }
65
+ };
66
+ const eraseCookie = (w: any, key: string, opts: CookieRemoveOpts = {}) => {
67
+ writeCookie(w, key, '', { path: opts.path ?? '/', domain: opts.domain, expires: new Date(0) });
68
+ };
69
+
70
+ // ─── IndexedDB backend (async; single db/store, raw strings) ───
71
+ const IDB_DB = 'swc-persist';
72
+ const IDB_STORE = 'kv';
73
+ const idbConns = new WeakMap<object, Promise<any>>();
74
+ const idbOpen = (w: any): Promise<any> => {
75
+ const idb = w?.indexedDB ?? (globalThis as any)?.indexedDB;
76
+ if (!idb) return Promise.reject(new Error('[SWC] indexedDB unavailable'));
77
+ const wKey = w ?? globalThis;
78
+ let p = idbConns.get(wKey);
79
+ if (!p) {
80
+ p = new Promise((resolve, reject) => {
81
+ const req = idb.open(IDB_DB, 1);
82
+ req.onupgradeneeded = () => req.result.createObjectStore(IDB_STORE);
83
+ req.onsuccess = () => resolve(req.result);
84
+ req.onerror = () => reject(req.error);
85
+ });
86
+ // 열기에 실패하면 캐시에서 지워서 다음 호출이 재시도할 수 있게 한다 — 그대로 캐싱해두면 한 번 실패로 영구히 막힌다.
87
+ p.catch(() => idbConns.delete(wKey));
88
+ idbConns.set(wKey, p);
89
+ }
90
+ return p;
91
+ };
92
+ const idbTx = async (w: any, mode: IDBTransactionMode, fn: (store: any) => IDBRequest): Promise<any> => {
93
+ const db = await idbOpen(w);
94
+ return new Promise((resolve, reject) => {
95
+ const tx = db.transaction(IDB_STORE, mode);
96
+ const req = fn(tx.objectStore(IDB_STORE));
97
+ req.onsuccess = () => resolve(req.result);
98
+ req.onerror = () => reject(req.error);
99
+ });
100
+ };
101
+ const idbGet = (w: any, key: string): Promise<string | null> =>
102
+ idbTx(w, 'readonly', (s) => s.get(key)).then((v) => (v === undefined ? null : v));
103
+ const idbSet = (w: any, key: string, raw: string): Promise<void> =>
104
+ idbTx(w, 'readwrite', (s) => s.put(raw, key)).then(() => undefined);
105
+ const idbDel = (w: any, key: string): Promise<void> =>
106
+ idbTx(w, 'readwrite', (s) => s.delete(key)).then(() => undefined);
107
+
108
+ const defSer = (v: any) => JSON.stringify(v);
109
+ const defDe = (s: string) => {
110
+ try { return JSON.parse(s); } catch { return s; }
111
+ };
112
+
113
+ export async function persistReadAsync(inst: any, key: string, opts: PersistOptions & { storage?: PersistStorage }): Promise<any> {
114
+ const storage = opts?.storage ?? 'local';
115
+ if (!isAsyncStorage(storage)) return persistRead(inst, key, opts);
116
+ const de = (opts as any)?.deserialize ?? defDe;
117
+ const raw = await idbGet(winOf(inst), key);
118
+ if (raw === null || raw === undefined) return undefined;
119
+ try { return de(raw); } catch { return raw; }
120
+ }
121
+ export async function persistWriteAsync(inst: any, key: string, value: any, opts: PersistOptions & { storage?: PersistStorage }): Promise<void> {
122
+ const storage = opts?.storage ?? 'local';
123
+ if (!isAsyncStorage(storage)) { persistWrite(inst, key, value, opts); return; }
124
+ if (value === undefined) { await persistRemoveAsync(inst, key, opts as any); return; }
125
+ const se = (opts as any)?.serialize ?? defSer;
126
+ await idbSet(winOf(inst), key, se(value));
127
+ }
128
+ export async function persistRemoveAsync(inst: any, key: string, opts: RemovePersistOptions & { storage?: PersistStorage } = {}): Promise<void> {
129
+ const storage = (opts as any)?.storage ?? 'local';
130
+ if (!isAsyncStorage(storage)) { persistRemove(inst, key, opts); return; }
131
+ await idbDel(winOf(inst), key);
132
+ }
133
+
134
+ const warnedAsyncField = new WeakSet<object>();
135
+ const isPromiseLike = (v: any) => v instanceof Promise || (v && typeof v.then === 'function');
136
+
137
+ export function persistRead(inst: any, key: string, opts: PersistOptions & { storage?: PersistStorage }): any {
138
+ const storage = opts?.storage ?? 'local';
139
+ const de = (opts as any)?.deserialize ?? defDe;
140
+ const w = winOf(inst);
141
+ let raw: string | null = null;
142
+ if (storage === 'cookie') raw = readCookie(w, key);
143
+ else raw = storeOf(w, storage)?.getItem(key) ?? null;
144
+ if (raw === null || raw === undefined) return undefined;
145
+ try { return de(raw); } catch { return raw; }
146
+ }
147
+ export function persistWrite(inst: any, key: string, value: any, opts: PersistOptions & { storage?: PersistStorage }) {
148
+ const storage = opts?.storage ?? 'local';
149
+ if (value === undefined) { persistRemove(inst, key, opts as any); return; }
150
+ const se = (opts as any)?.serialize ?? defSer;
151
+ const raw = se(value);
152
+ const w = winOf(inst);
153
+ if (storage === 'cookie') writeCookie(w, key, raw, opts as CookieOpts);
154
+ else {
155
+ try { storeOf(w, storage)?.setItem(key, raw); }
156
+ catch (e) { console.warn('[SWC] persistState write failed:', e); }
157
+ }
158
+ }
159
+ export function persistRemove(inst: any, key: string, opts: RemovePersistOptions & { storage?: PersistStorage } = {}) {
160
+ const storage = (opts as any)?.storage ?? 'local';
161
+ const w = winOf(inst);
162
+ if (storage === 'cookie') eraseCookie(w, key, opts as CookieRemoveOpts);
163
+ else {
164
+ try { storeOf(w, storage)?.removeItem(key); } catch { /* ignore */ }
165
+ }
166
+ }
167
+
168
+ const pickValue = (ret: any, valueKey?: string | symbol) =>
169
+ valueKey !== undefined ? (ret && typeof ret === 'object' ? ret[valueKey] : undefined) : ret;
170
+
171
+ function defineField(targetObj: any, propertyKey: string | symbol, key: string, opts: PersistOptions & { storage?: PersistStorage }) {
172
+ const slot = Symbol(`persist:${String(propertyKey)}`);
173
+ const initialized = Symbol(`persist:init:${String(propertyKey)}`);
174
+ const storage = (opts as any)?.storage ?? 'local';
175
+ const async = isAsyncStorage(storage);
176
+ // Runtime guard: a getter cannot await — the first read resolves behind.
177
+ // Warn once per class so the flicker (initial → restored) is a conscious choice.
178
+ if (async && !warnedAsyncField.has(targetObj)) {
179
+ warnedAsyncField.add(targetObj);
180
+ console.warn(`[SWC] @persistState(${storage}) on field "${String(propertyKey)}" resolves asynchronously: first read returns the initializer, the stored value lands right after. Prefer calling persistReadAsync directly or Promise-typed fields for async backends.`);
181
+ }
182
+ Object.defineProperty(targetObj, propertyKey, {
183
+ get(this: any) {
184
+ const own = Object.getOwnPropertyDescriptor(this, propertyKey);
185
+ if (own && 'value' in own) {
186
+ delete this[propertyKey];
187
+ const initVal = own.value;
188
+ // Promise initializer: persist its resolution, expose the promise meanwhile.
189
+ // Stored value still wins if present — mirrors the non-Promise branch below.
190
+ if (async && isPromiseLike(initVal)) {
191
+ this[slot] = initVal;
192
+ this[initialized] = true;
193
+ void persistReadAsync(this, key, opts).then((stored) => {
194
+ if (stored !== undefined) {
195
+ this[slot] = Promise.resolve(stored);
196
+ } else {
197
+ void (initVal as Promise<any>).then((v) => {
198
+ this[slot] = v;
199
+ void persistWriteAsync(this, key, v, opts).catch((e) => console.warn('[SWC] persistState write failed:', e));
200
+ });
201
+ }
202
+ }).catch(() => undefined);
203
+ return this[slot];
204
+ }
205
+ this[slot] = initVal;
206
+ this[initialized] = true;
207
+ // initializer wins on first boot? No — stored value wins if present.
208
+ if (async) {
209
+ // cannot await in a getter — read behind, update the slot when it lands.
210
+ void persistReadAsync(this, key, opts).then((stored) => {
211
+ if (stored !== undefined) this[slot] = stored;
212
+ else void persistWriteAsync(this, key, this[slot], opts).catch((e) => console.warn('[SWC] persistState write failed:', e));
213
+ }).catch(() => undefined);
214
+ } else {
215
+ const stored = persistRead(this, key, opts);
216
+ if (stored !== undefined) this[slot] = stored;
217
+ else persistWrite(this, key, this[slot], opts);
218
+ }
219
+ return this[slot];
220
+ }
221
+ if (!this[initialized]) {
222
+ this[initialized] = true;
223
+ if (async) {
224
+ void persistReadAsync(this, key, opts).then((stored) => {
225
+ if (stored !== undefined) this[slot] = stored;
226
+ else if (this[slot] !== undefined) void persistWriteAsync(this, key, this[slot], opts).catch((e) => console.warn('[SWC] persistState write failed:', e));
227
+ }).catch(() => undefined);
228
+ } else {
229
+ const stored = persistRead(this, key, opts);
230
+ this[slot] = stored !== undefined ? stored : this[slot];
231
+ if (stored === undefined && this[slot] !== undefined) persistWrite(this, key, this[slot], opts);
232
+ }
233
+ }
234
+ return this[slot];
235
+ },
236
+ set(this: any, nv: any) {
237
+ if (async) {
238
+ const firstCall = !this[initialized];
239
+ this[slot] = nv;
240
+ this[initialized] = true;
241
+ if (firstCall) {
242
+ // 필드 초기화의 첫 set — 생성자 할당(또는 초기값 없는 필드의 첫 접근)이라 저장된 값이 있으면 그게 이긴다.
243
+ void persistReadAsync(this, key, opts).then((stored) => {
244
+ if (stored !== undefined) { this[slot] = isPromiseLike(nv) ? Promise.resolve(stored) : stored; return; }
245
+ if (isPromiseLike(nv)) {
246
+ void (nv as Promise<any>).then((v) => {
247
+ this[slot] = v;
248
+ void persistWriteAsync(this, key, v, opts).catch((e) => console.warn('[SWC] persistState write failed:', e));
249
+ });
250
+ } else if (nv !== undefined) {
251
+ void persistWriteAsync(this, key, nv, opts).catch((e) => console.warn('[SWC] persistState write failed:', e));
252
+ }
253
+ }).catch(() => undefined);
254
+ return;
255
+ }
256
+ // 생성자 이후의 명시적 재할당 — 의도된 덮어쓰기이므로 그대로 저장.
257
+ if (isPromiseLike(nv)) {
258
+ void (nv as Promise<any>).then((v) => {
259
+ this[slot] = v;
260
+ void persistWriteAsync(this, key, v, opts).catch((e) => console.warn('[SWC] persistState write failed:', e));
261
+ });
262
+ } else if (nv === undefined) {
263
+ void persistRemoveAsync(this, key, opts as any).catch(() => undefined);
264
+ } else {
265
+ void persistWriteAsync(this, key, nv, opts).catch((e) => console.warn('[SWC] persistState write failed:', e));
266
+ }
267
+ return;
268
+ }
269
+ // field initializer runs through the setter at construction and would
270
+ // clobber the stored value — stored wins on first write.
271
+ if (!this[initialized]) {
272
+ const stored = persistRead(this, key, opts);
273
+ this[initialized] = true;
274
+ if (stored !== undefined) { this[slot] = stored; return; }
275
+ }
276
+ this[slot] = nv;
277
+ this[initialized] = true;
278
+ persistWrite(this, key, nv, opts);
279
+ },
280
+ enumerable: true,
281
+ configurable: true,
282
+ });
283
+ }
284
+
285
+ function wrapMethod(targetObj: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor, key: string, opts: PersistOptions & { storage?: PersistStorage }) {
286
+ const original = descriptor.value;
287
+ if ((original as any)[PERSIST_WRAPPED]) return descriptor;
288
+ const storage = (opts as any)?.storage ?? 'local';
289
+ const async = isAsyncStorage(storage);
290
+ const wrapped = function (this: any, ...args: any[]) {
291
+ const res = (original as any).apply(this, args);
292
+ const doPersist = (v: any) => {
293
+ const val = pickValue(v, (opts as any)?.valueKey);
294
+ if (val === undefined) return;
295
+ if (async) void persistWriteAsync(this, key, val, opts).catch((e) => console.warn('[SWC] persistState write failed:', e));
296
+ else persistWrite(this, key, val, opts);
297
+ };
298
+ if (res instanceof Promise) return res.then((v: any) => { doPersist(v); return v; });
299
+ doPersist(res);
300
+ return res;
301
+ };
302
+ (wrapped as any)[PERSIST_WRAPPED] = true;
303
+ descriptor.value = wrapped;
304
+ return descriptor;
305
+ }
306
+
307
+ // ─── persistState (field + method, bareable) ───
308
+ export function persistState(target: Object, propertyKey: string | symbol): void;
309
+ export function persistState(key?: string, options?: PersistOptions): PropertyDecorator & MethodDecorator;
310
+ export function persistState(keyOrTarget?: string | Object, optionsOrKey?: PersistOptions | string | symbol, descriptor?: PropertyDescriptor): any {
311
+ // bare: @persistState
312
+ if (typeof keyOrTarget === 'object' && (typeof optionsOrKey === 'string' || typeof optionsOrKey === 'symbol')) {
313
+ const targetObj = keyOrTarget as Object;
314
+ const propertyKey = optionsOrKey as string | symbol;
315
+ if (descriptor) return wrapMethod(targetObj, propertyKey, descriptor, String(propertyKey), { storage: 'local' });
316
+ defineField(targetObj, propertyKey, String(propertyKey), { storage: 'local' });
317
+ return;
318
+ }
319
+ const key = typeof keyOrTarget === 'string' ? keyOrTarget : undefined;
320
+ const opts = (typeof keyOrTarget === 'object' && keyOrTarget !== null ? {} : (optionsOrKey as PersistOptions ?? {})) as PersistOptions & { storage?: PersistStorage };
321
+ return (targetObj: Object, propertyKey: string | symbol, desc?: PropertyDescriptor): any => {
322
+ const k = key ?? String(propertyKey);
323
+ if (desc) return wrapMethod(targetObj, propertyKey, desc, k, opts);
324
+ defineField(targetObj, propertyKey, k, opts);
325
+ };
326
+ }
327
+
328
+ // thin aliases — storage preset
329
+ const alias = (storage: PersistStorage, extra: any = {}) => {
330
+ const fn = (a?: any, b?: any, c?: PropertyDescriptor): any => {
331
+ // bare: @localStorage
332
+ if (typeof b === 'string' || typeof b === 'symbol') {
333
+ if (c) return wrapMethod(a, b, c, String(b), { storage, ...extra });
334
+ defineField(a, b, String(b), { storage, ...extra });
335
+ return;
336
+ }
337
+ if (typeof a === 'string') return persistState(a, { ...(b ?? {}), storage, ...extra });
338
+ if (a && typeof a === 'object' && b === undefined && c === undefined) {
339
+ // @localStorage({...}) options form
340
+ return persistState(undefined, { ...(a ?? {}), storage, ...extra });
341
+ }
342
+ return (persistState(undefined, { storage, ...extra }) as any)(a, b, c!);
343
+ };
344
+ return fn;
345
+ };
346
+ export const localStorage = alias('local');
347
+ export const sessionStorage = alias('session');
348
+ export const cookie = alias('cookie', { path: '/' });
349
+ /** Async backend — see runtime guards in wrapMethod/defineField. */
350
+ export const indexedDb = alias('indexeddb');
351
+
352
+ // ─── remove* (method only, Declaration = action) ───
353
+ export function removePersistState(key?: string, options?: RemovePersistOptions): MethodDecorator;
354
+ export function removePersistState(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): any;
355
+ export function removePersistState(a?: any, b?: any, c?: PropertyDescriptor): any {
356
+ if (c) {
357
+ const propertyKey = b as string | symbol;
358
+ const orig = c.value;
359
+ c.value = function (...args: any[]) {
360
+ const res = orig.apply(this, args);
361
+ const doRemove = (v: any) => {
362
+ const go = (a as RemovePersistOptions)?.valueKey !== undefined
363
+ ? (v && typeof v === 'object' ? v[(a as any).valueKey] : undefined)
364
+ : true;
365
+ if (!go) return;
366
+ persistRemove(this, String(propertyKey), (a as any) ?? {});
367
+ };
368
+ if (res instanceof Promise) return res.then((v: any) => { doRemove(v); return v; });
369
+ doRemove(res);
370
+ return res;
371
+ };
372
+ return c;
373
+ }
374
+ const key = typeof a === 'string' ? a : undefined;
375
+ const opts = (typeof a === 'object' && a !== null ? a : (b as any ?? {})) as RemovePersistOptions;
376
+ return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): any => {
377
+ const k = key ?? String(propertyKey);
378
+ const orig = descriptor.value;
379
+ descriptor.value = function (...args: any[]) {
380
+ const res = orig.apply(this, args);
381
+ const doRemove = (v: any) => {
382
+ const vk = (opts as any)?.valueKey;
383
+ const go = vk !== undefined ? (v && typeof v === 'object' ? v[vk] : undefined) : true;
384
+ if (!go) return;
385
+ persistRemove(this, k, opts);
386
+ };
387
+ if (res instanceof Promise) return res.then((v: any) => { doRemove(v); return v; });
388
+ doRemove(res);
389
+ return res;
390
+ };
391
+ return descriptor;
392
+ };
393
+ }
394
+ const removeAlias = (storage: PersistStorage) => (a?: any, b?: any, c?: PropertyDescriptor): any => {
395
+ if (c) {
396
+ const key = String(b);
397
+ return removePersistState(key, { storage } as any)(a!, b!, c);
398
+ }
399
+ if (typeof a === 'string') return removePersistState(a, { ...(b ?? {}), storage } as any);
400
+ if (a && typeof a === 'object') return removePersistState(undefined, { ...(a ?? {}), storage } as any);
401
+ // bare @removeLocalStorage
402
+ return (t: Object, pk: string | symbol, d: PropertyDescriptor): any =>
403
+ removePersistState(String(pk), { storage } as any)(t, pk, d);
404
+ };
405
+ export const removeLocalStorage = removeAlias('local');
406
+ export const removeSessionStorage = removeAlias('session');
407
+ export const removeCookie = removeAlias('cookie');
408
+ export const removeIndexedDb = removeAlias('indexeddb');
@@ -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
  // 공통 옵션 — root·delegate 없음
6
6
  export interface ResizeObserverBaseOptions {
@@ -204,12 +204,11 @@ export class ResizeObserverLifeCycler implements ElementDefineLifeCycler {
204
204
  void (async () => {
205
205
  const helper = SwcUtils.getHelperAndHostSet(inst);
206
206
  if (opts.filter && !(await opts.filter(matchedEls, { currentThis: inst, helper }))) return;
207
- const hostSet = SwcUtils.getHostSet(inst);
208
- const helperSet = SwcUtils.getHelperSet(helperHostSet.$w);
207
+ const common = buildCommonKindValues(inst, helperHostSet.$w);
209
208
  // entries 도 이 메서드 대상 것만 (옵저버는 공유라 배치에 다른 대상 엔트리가 섞인다)
210
- const legacyArgs = [matchedEls, entries.filter(e => matchedEls.includes(e.target as HTMLElement)), obs, { ...hostSet, $root: root }];
209
+ const legacyArgs = [matchedEls, entries.filter(e => matchedEls.includes(e.target as HTMLElement)), obs, { ...common.hostSet, $root: root }];
211
210
  const buildArgs = (beforeReturn: any) => buildSwcParameterArgs(inst, m.propertyKey, {
212
- hostSet, helperHostSet: helper, helperSet, resizeObserverBeforeReturn: beforeReturn
211
+ ...common, resizeObserverBeforeReturn: 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,7 +1,7 @@
1
1
  import { ReflectUtils } from '@dooboostore/core';
2
2
  import { ensureInit } from './elementDefine';
3
3
  import { SwcUtils } from '../utils/Utils';
4
- import { buildSwcParameterArgs } from './parameter';
4
+ import { buildSwcParameterArgs, buildCommonKindValues } from './parameter';
5
5
  import { ElementDefineLifeCycler, HelperHostSet } from '../types';
6
6
 
7
7
  export const SET_INTERVAL_METADATA_KEY = Symbol.for('simple-web-component:set-interval');
@@ -108,10 +108,9 @@ export class SetIntervalLifeCycler implements ElementDefineLifeCycler {
108
108
  const helper = SwcUtils.getHelperAndHostSet(inst);
109
109
  if (meta.options.filter && !(await meta.options.filter(helper))) return;
110
110
  const paramArgs = meta.options.parameter?.(helperHostSet) ?? [];
111
- const hostSet = SwcUtils.getHostSet(inst);
112
- const helperSet = SwcUtils.getHelperSet(helperHostSet.$w);
111
+ const common = buildCommonKindValues(inst, helperHostSet.$w);
113
112
  const buildArgs = (beforeReturn: any) => buildSwcParameterArgs(inst, meta.propertyKey, {
114
- hostSet, helperHostSet: helper, helperSet, setIntervalBeforeReturn: beforeReturn
113
+ ...common, setIntervalBeforeReturn: beforeReturn
115
114
  }, [...paramArgs, beforeReturn]);
116
115
  let args = buildArgs(undefined);
117
116
  if (meta.options.before) { const br = await meta.options.before(helper); args = buildArgs(br); }
@@ -1,7 +1,7 @@
1
1
  import { ReflectUtils } from '@dooboostore/core';
2
2
  import { ensureInit } from './elementDefine';
3
3
  import { SwcUtils } from '../utils/Utils';
4
- import { buildSwcParameterArgs } from './parameter';
4
+ import { buildSwcParameterArgs, buildCommonKindValues } from './parameter';
5
5
  import { ElementDefineLifeCycler, HelperHostSet } from '../types';
6
6
 
7
7
  export const SET_TIMEOUT_METADATA_KEY = Symbol.for('simple-web-component:set-timeout');
@@ -111,10 +111,9 @@ export class SetTimeoutLifeCycler implements ElementDefineLifeCycler {
111
111
  const helper = SwcUtils.getHelperAndHostSet(inst);
112
112
  if (meta.options.filter && !(await meta.options.filter(helper))) return;
113
113
  const paramArgs = meta.options.parameter?.(helperHostSet) ?? [];
114
- const hostSet = SwcUtils.getHostSet(inst);
115
- const helperSet = SwcUtils.getHelperSet(helperHostSet.$w);
114
+ const common = buildCommonKindValues(inst, helperHostSet.$w);
116
115
  const buildArgs = (beforeReturn: any) => buildSwcParameterArgs(inst, meta.propertyKey, {
117
- hostSet, helperHostSet: helper, helperSet, setTimeoutBeforeReturn: beforeReturn
116
+ ...common, setTimeoutBeforeReturn: beforeReturn
118
117
  }, [...paramArgs, beforeReturn]);
119
118
  let args = buildArgs(undefined);
120
119
  if (meta.options.before) { const br = await meta.options.before(helper); args = buildArgs(br); }
@@ -6,7 +6,7 @@ export const SUBSCRIBE_SWC_APP_MESSAGE_METADATA_KEY = Symbol.for('simple-web-com
6
6
  export interface SwcAppMessageOptions<Data = any, BeforeReturn = any, Result = any> {
7
7
  /** 메시지 수신 시 핸들러 실행 여부 게이트. Promise<boolean>도 되어 async 판정 가능. false면 스킵. */
8
8
  filter?: (message: SwcAppMessage<Data>, currentThis: any) => boolean | Promise<boolean>;
9
- /** filter 통과 후 핸들러 직전 훅. await되고, 리턴값은 @appMessageBeforeReturn 으로 핸들러에 주입된다. */
9
+ /** filter 통과 후 핸들러 직전 훅. await되고, 리턴값은 @swcAppMessageBeforeReturn 으로 핸들러에 주입된다. */
10
10
  before?: (message: SwcAppMessage<Data>, currentThis: any) => BeforeReturn | Promise<BeforeReturn>;
11
11
  /** 핸들러가 성공/실패해도 항상 실행되는 정리 훅. ctx로 핸들러 인자/결과/에러를 받는다. 에러는 삼키지 않고 전파. */
12
12
  finally?: (message: SwcAppMessage<Data>, currentThis: any, ctx: { args: any[]; result?: Result; error?: any }) => any | Promise<any>;
@@ -1,5 +1,5 @@
1
1
  import { SwcAppInterface, SwcAppMessage, SwcAppMessageObserveOptions, SwcElement } from '../types';
2
- import { APPLY_NODE_METADATA_KEY, findAllLifecycleMetadata, getSubscribeSwcAppMessageMetadata, getSubscribeSwcAppRouteChangeMetadata, ON_CONNECTED_SWC_APP_METADATA_KEY, SUBSCRIBE_SWC_APP_ROUTE_CHANGE_METADATA_KEY, buildSwcParameterArgs } from '../decorators';
2
+ import { APPLY_NODE_METADATA_KEY, findAllLifecycleMetadata, getSubscribeSwcAppMessageMetadata, getSubscribeSwcAppRouteChangeMetadata, ON_CONNECTED_SWC_APP_METADATA_KEY, SUBSCRIBE_SWC_APP_ROUTE_CHANGE_METADATA_KEY, buildSwcParameterArgs, getSwcRouteFieldMetadata, getSwcAppMessageFieldMetadata } from '../decorators';
3
3
  import { SwcAppEngine, SwcAttributeConfigType, SwcConfigType } from '../SwcAppEngine';
4
4
  import { debounceTimeIntervalLock, FunctionUtils, Subscription, Subject, Observable } from '@dooboostore/core';
5
5
  import {RouterEventType, ValidUtils} from '@dooboostore/core-web';
@@ -49,6 +49,14 @@ const matchRoute = (pathOption: any, instance: any, re: { path?: string }): { pa
49
49
  return null;
50
50
  };
51
51
 
52
+ /** 같은 값이면 다시 넣지 않는다 — 필드에 setter(@state 등)가 있으면 상관없는 라우트 변경마다 불리지 않게 */
53
+ const sameRouteValue = (a: any, b: any) => {
54
+ if (a === b) return true;
55
+ if (a instanceof URLSearchParams && b instanceof URLSearchParams) return a.toString() === b.toString();
56
+ if (a && b && typeof a === 'object' && typeof b === 'object') return JSON.stringify(a) === JSON.stringify(b);
57
+ return false;
58
+ };
59
+
52
60
  export const isSSR = (i: HTMLElement) => {
53
61
  return i.hasAttribute('swc-use-ssr');
54
62
  };
@@ -119,6 +127,10 @@ export function SwcAppMixin<T extends { new (...args: any[]): HTMLElement }>(Bas
119
127
  async _invokeRouteChangeSubscribers(instance: HTMLElement, re: RouterEventType, phase: 'live' | 'connected' | 'connectedDone' = 'live') {
120
128
  if (instance.isConnected === false) return;
121
129
 
130
+ // 필드 주입은 핸들러보다 먼저 — 핸들러 안에서 this.필드가 이미 이번 라우트 값이다.
131
+ // connectedDone 재생은 같은 라우트를 connected 때 이미 넣었으므로 건너뛴다.
132
+ if (phase !== 'connectedDone') this._applyRouteFields(instance, re);
133
+
122
134
  this._routeChangeInvocations++;
123
135
 
124
136
  if (this._childrenRouteChangedInterval) {
@@ -223,6 +235,25 @@ export function SwcAppMixin<T extends { new (...args: any[]): HTMLElement }>(Bas
223
235
  }
224
236
  }
225
237
 
238
+ /**
239
+ * 필드에 붙은 라우트 파라미터 데코레이터(@swcAppRouteQueryParam('tag') tag, @swcAppRoutePathVariable('id', { path }) id …)에
240
+ * 지금 라우트 값을 넣는다. query 는 URL 에서 바로, path 변수는 필드의 path 패턴에 매칭해서 (안 맞으면 비어 있음 — 핸들러 인자와 같은 규칙).
241
+ */
242
+ _applyRouteFields(instance: any, re: RouterEventType) {
243
+ const fields = getSwcRouteFieldMetadata(instance);
244
+ if (!fields.length) return;
245
+ const sp: URLSearchParams = re.searchParams ?? new URLSearchParams(re.search ?? '');
246
+ const queryValues: any = routeParamValues(sp, {});
247
+ for (const field of fields) {
248
+ const values: any = field.kind.includes('PathVariable')
249
+ ? routeParamValues(sp, matchRoute(field.path, instance, re)?.pathDataAll ?? {})
250
+ : queryValues;
251
+ const resolver = values[field.kind];
252
+ const value = field.key !== undefined && typeof resolver === 'function' ? resolver(field.key) : resolver;
253
+ if (!sameRouteValue(instance[field.propertyKey], value)) instance[field.propertyKey] = value;
254
+ }
255
+ }
256
+
226
257
  /** 라우트 핸들러 인자: @swcAppRouterEvent / host 계열 / @routeChangeBeforeReturn / 라우트 파라미터(query·path 변수) */
227
258
  _buildRouteArgs(instance: any, methodName: string | symbol, routeEventValue: any, pathDataAll: { [k: string]: string[] }, beforeReturn: any) {
228
259
  const instanceHelperHostSet = SwcUtils.getHelperAndHostSet(instance, this.config.window);
@@ -232,6 +263,9 @@ export function SwcAppMixin<T extends { new (...args: any[]): HTMLElement }>(Bas
232
263
  methodName,
233
264
  {
234
265
  swcAppRouterEvent: routeEventValue,
266
+ swcAppRouter: this.router,
267
+ swcAppSimpleApplication: this.simpleApplication,
268
+ swcAppHost: this,
235
269
  hostSet: instanceHelperHostSet,
236
270
  helperHostSet: instanceHelperHostSet,
237
271
  helperSet: SwcUtils.getHelperSet(this.config.window),
@@ -331,7 +365,8 @@ export function SwcAppMixin<T extends { new (...args: any[]): HTMLElement }>(Bas
331
365
 
332
366
  if (this.simpleApplication) this._registerLeaveGuards(instance);
333
367
 
334
- // replay 버퍼가 있으면 신규 인스턴스에만 재생 (fire-and-forget, 연결 블로킹 안 함)
368
+ // replay 버퍼가 있으면 신규 인스턴스에만 재생 (fire-and-forget, 연결 블로킹 안 함). 필드 먼저.
369
+ this._replayMessageFieldsTo(instance);
335
370
  this._replayMessagesTo(instance);
336
371
 
337
372
  const isBrowser = ValidUtils.isBrowser();
@@ -484,6 +519,8 @@ export function SwcAppMixin<T extends { new (...args: any[]): HTMLElement }>(Bas
484
519
  this._replayedMessages.set(message.type, buf.slice(-Math.max(1, this.config?.messageReplayBufferSize ?? 10)));
485
520
  }
486
521
  this._swc_connected_instance.forEach((instance: any) => {
522
+ // 필드 먼저 — 같은 요소의 구독 핸들러 안에서 this.필드가 이미 이 메시지다
523
+ this._applyMessageFields(instance, message);
487
524
  this._invokeMessageSubscribers(instance, message);
488
525
  });
489
526
  this._messageSubject.next(message);
@@ -534,6 +571,25 @@ export function SwcAppMixin<T extends { new (...args: any[]): HTMLElement }>(Bas
534
571
  return manager.executeBindParameterSimPromise({ target: this, targetKey: methodName });
535
572
  }
536
573
 
574
+ /** 필드에 붙은 @swcAppMessage — 타입이 맞으면(타입 없으면 전부) 그 필드에 메시지를 넣는다 */
575
+ _applyMessageFields(instance: any, message: SwcAppMessage, fields = getSwcAppMessageFieldMetadata(instance)) {
576
+ for (const field of fields) {
577
+ if (!field.messageType || field.messageType === message.type) instance[field.propertyKey] = message;
578
+ }
579
+ }
580
+
581
+ /**
582
+ * 새로 붙은 인스턴스의 @swcAppMessage 필드를 버퍼로 채운다 (렌더 전). @subscribeSwcAppMessage 와 같은 규칙:
583
+ * 'behavior' = 마지막 1개, 'replay' = 버퍼 전체 시간순(결국 마지막 값), 미지정·'subject' = 채우지 않음. 타입이 있을 때만.
584
+ */
585
+ _replayMessageFieldsTo(instance: any) {
586
+ for (const field of getSwcAppMessageFieldMetadata(instance)) {
587
+ if (!field.messageType || (field.subject !== 'behavior' && field.subject !== 'replay')) continue;
588
+ const buf = this._replayedMessages.get(field.messageType) ?? [];
589
+ for (const message of field.subject === 'behavior' ? buf.slice(-1) : buf) this._applyMessageFields(instance, message, [field]);
590
+ }
591
+ }
592
+
537
593
  // 신규 연결 인스턴스 1곳에만 버퍼 재생. 구독자별 subject 선언 기준:
538
594
  // - 'behavior': 마지막 1개 / 'replay': 버퍼 전체 시간순 / 미지정·'subject': live만 (재생 안 함)
539
595
  // - trigger 'connectedDone'인 구독은 _connectedDone(render 완료 후)에서 재생. phase로 구분.
@@ -580,17 +636,20 @@ export function SwcAppMixin<T extends { new (...args: any[]): HTMLElement }>(Bas
580
636
 
581
637
  const instanceHelperHostSet = SwcUtils.getHelperAndHostSet(instance, this.config.window);
582
638
  const instanceHelperSet = SwcUtils.getHelperSet(this.config.window);
583
- // before를 먼저 돌려 리턴값을 @appMessageBeforeReturn 으로 주입할 수 있게 캡처.
639
+ // before를 먼저 돌려 리턴값을 @swcAppMessageBeforeReturn 으로 주입할 수 있게 캡처.
584
640
  const beforeReturn = before ? await before(message, instance) : undefined;
585
641
  const args = buildSwcParameterArgs(
586
642
  instance,
587
643
  methodName,
588
644
  {
589
- appMessage: message,
645
+ swcAppMessage: message,
646
+ swcAppRouter: this.router,
647
+ swcAppSimpleApplication: this.simpleApplication,
648
+ swcAppHost: this,
590
649
  hostSet: instanceHelperHostSet,
591
650
  helperHostSet: instanceHelperHostSet,
592
651
  helperSet: instanceHelperSet,
593
- appMessageBeforeReturn: beforeReturn
652
+ swcAppMessageBeforeReturn: beforeReturn
594
653
  },
595
654
  [message]
596
655
  );