@praxisjs/decorators 1.7.0 → 2.0.1

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 (266) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/component/component.d.ts +1 -1
  3. package/dist/component/component.d.ts.map +1 -1
  4. package/dist/component/component.js +8 -15
  5. package/dist/component/component.js.map +1 -1
  6. package/dist/component/lazy.d.ts +1 -1
  7. package/dist/component/lazy.d.ts.map +1 -1
  8. package/dist/component/lazy.js +44 -36
  9. package/dist/component/lazy.js.map +1 -1
  10. package/dist/create-class-decorator.d.ts +27 -5
  11. package/dist/create-class-decorator.d.ts.map +1 -1
  12. package/dist/create-class-decorator.js +53 -18
  13. package/dist/create-class-decorator.js.map +1 -1
  14. package/dist/create-field-decorator.d.ts +42 -3
  15. package/dist/create-field-decorator.d.ts.map +1 -1
  16. package/dist/create-field-decorator.js +63 -27
  17. package/dist/create-field-decorator.js.map +1 -1
  18. package/dist/create-getter-decorator.d.ts +81 -16
  19. package/dist/create-getter-decorator.d.ts.map +1 -1
  20. package/dist/create-getter-decorator.js +127 -15
  21. package/dist/create-getter-decorator.js.map +1 -1
  22. package/dist/create-lifecycle-method-decorator.d.ts +21 -3
  23. package/dist/create-lifecycle-method-decorator.d.ts.map +1 -1
  24. package/dist/create-lifecycle-method-decorator.js +25 -18
  25. package/dist/create-lifecycle-method-decorator.js.map +1 -1
  26. package/dist/create-method-decorator.d.ts +43 -3
  27. package/dist/create-method-decorator.d.ts.map +1 -1
  28. package/dist/create-method-decorator.js +49 -8
  29. package/dist/create-method-decorator.js.map +1 -1
  30. package/dist/events/command.d.ts +2 -1
  31. package/dist/events/command.d.ts.map +1 -1
  32. package/dist/events/command.js +10 -8
  33. package/dist/events/command.js.map +1 -1
  34. package/dist/events/emit.d.ts +2 -2
  35. package/dist/events/emit.d.ts.map +1 -1
  36. package/dist/events/emit.js +18 -24
  37. package/dist/events/emit.js.map +1 -1
  38. package/dist/events/on-command.d.ts +2 -1
  39. package/dist/events/on-command.d.ts.map +1 -1
  40. package/dist/events/on-command.js +12 -14
  41. package/dist/events/on-command.js.map +1 -1
  42. package/dist/field.d.ts +51 -0
  43. package/dist/field.d.ts.map +1 -0
  44. package/dist/field.js +82 -0
  45. package/dist/field.js.map +1 -0
  46. package/dist/functions/bind.d.ts +2 -1
  47. package/dist/functions/bind.d.ts.map +1 -1
  48. package/dist/functions/bind.js +4 -4
  49. package/dist/functions/bind.js.map +1 -1
  50. package/dist/functions/debounce.d.ts +2 -2
  51. package/dist/functions/debounce.d.ts.map +1 -1
  52. package/dist/functions/debounce.js +25 -23
  53. package/dist/functions/debounce.js.map +1 -1
  54. package/dist/functions/log.d.ts +2 -1
  55. package/dist/functions/log.d.ts.map +1 -1
  56. package/dist/functions/log.js +38 -40
  57. package/dist/functions/log.js.map +1 -1
  58. package/dist/functions/memo.d.ts +2 -1
  59. package/dist/functions/memo.d.ts.map +1 -1
  60. package/dist/functions/memo.js +41 -40
  61. package/dist/functions/memo.js.map +1 -1
  62. package/dist/functions/once.d.ts +2 -1
  63. package/dist/functions/once.d.ts.map +1 -1
  64. package/dist/functions/once.js +10 -14
  65. package/dist/functions/once.js.map +1 -1
  66. package/dist/functions/retry.d.ts +2 -1
  67. package/dist/functions/retry.d.ts.map +1 -1
  68. package/dist/functions/retry.js +21 -23
  69. package/dist/functions/retry.js.map +1 -1
  70. package/dist/functions/throttle.d.ts +2 -1
  71. package/dist/functions/throttle.d.ts.map +1 -1
  72. package/dist/functions/throttle.js +12 -14
  73. package/dist/functions/throttle.js.map +1 -1
  74. package/dist/functions/until.d.ts +2 -1
  75. package/dist/functions/until.d.ts.map +1 -1
  76. package/dist/functions/until.js +13 -14
  77. package/dist/functions/until.js.map +1 -1
  78. package/dist/functions/watch.d.ts +1 -1
  79. package/dist/functions/watch.d.ts.map +1 -1
  80. package/dist/functions/watch.js +36 -34
  81. package/dist/functions/watch.js.map +1 -1
  82. package/dist/functions/when.d.ts +1 -1
  83. package/dist/functions/when.d.ts.map +1 -1
  84. package/dist/functions/when.js +19 -21
  85. package/dist/functions/when.js.map +1 -1
  86. package/dist/index.d.ts +5 -5
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +2 -2
  89. package/dist/index.js.map +1 -1
  90. package/dist/lifecycle.d.ts +24 -0
  91. package/dist/lifecycle.d.ts.map +1 -0
  92. package/dist/lifecycle.js +55 -0
  93. package/dist/lifecycle.js.map +1 -0
  94. package/dist/properties/compose.d.ts +3 -2
  95. package/dist/properties/compose.d.ts.map +1 -1
  96. package/dist/properties/compose.js +33 -19
  97. package/dist/properties/compose.js.map +1 -1
  98. package/dist/properties/computed.d.ts +12 -10
  99. package/dist/properties/computed.d.ts.map +1 -1
  100. package/dist/properties/computed.js +26 -21
  101. package/dist/properties/computed.js.map +1 -1
  102. package/dist/properties/deep-state.d.ts +2 -1
  103. package/dist/properties/deep-state.d.ts.map +1 -1
  104. package/dist/properties/deep-state.js +66 -60
  105. package/dist/properties/deep-state.js.map +1 -1
  106. package/dist/properties/function-prop.d.ts +2 -1
  107. package/dist/properties/function-prop.d.ts.map +1 -1
  108. package/dist/properties/function-prop.js +20 -22
  109. package/dist/properties/function-prop.js.map +1 -1
  110. package/dist/properties/history.d.ts +2 -2
  111. package/dist/properties/history.d.ts.map +1 -1
  112. package/dist/properties/history.js +34 -33
  113. package/dist/properties/history.js.map +1 -1
  114. package/dist/properties/index.d.ts +1 -0
  115. package/dist/properties/index.d.ts.map +1 -1
  116. package/dist/properties/index.js +1 -0
  117. package/dist/properties/index.js.map +1 -1
  118. package/dist/properties/persisted.d.ts +2 -1
  119. package/dist/properties/persisted.d.ts.map +1 -1
  120. package/dist/properties/persisted.js +24 -18
  121. package/dist/properties/persisted.js.map +1 -1
  122. package/dist/properties/prop.d.ts +2 -1
  123. package/dist/properties/prop.d.ts.map +1 -1
  124. package/dist/properties/prop.js +26 -22
  125. package/dist/properties/prop.js.map +1 -1
  126. package/dist/properties/ref.d.ts +2 -1
  127. package/dist/properties/ref.d.ts.map +1 -1
  128. package/dist/properties/ref.js +11 -8
  129. package/dist/properties/ref.js.map +1 -1
  130. package/dist/properties/resource.d.ts.map +1 -1
  131. package/dist/properties/resource.js +21 -16
  132. package/dist/properties/resource.js.map +1 -1
  133. package/dist/properties/selector.d.ts +12 -0
  134. package/dist/properties/selector.d.ts.map +1 -0
  135. package/dist/properties/selector.js +25 -0
  136. package/dist/properties/selector.js.map +1 -0
  137. package/dist/properties/slot.d.ts +2 -1
  138. package/dist/properties/slot.d.ts.map +1 -1
  139. package/dist/properties/slot.js +22 -20
  140. package/dist/properties/slot.js.map +1 -1
  141. package/dist/properties/state.d.ts.map +1 -1
  142. package/dist/properties/state.js +27 -16
  143. package/dist/properties/state.js.map +1 -1
  144. package/dist/properties/synced.d.ts +2 -1
  145. package/dist/properties/synced.d.ts.map +1 -1
  146. package/dist/properties/synced.js +27 -19
  147. package/dist/properties/synced.js.map +1 -1
  148. package/dist/shared-method.d.ts +14 -0
  149. package/dist/shared-method.d.ts.map +1 -0
  150. package/dist/shared-method.js +87 -0
  151. package/dist/shared-method.js.map +1 -0
  152. package/package.json +14 -5
  153. package/src/component/component.ts +8 -27
  154. package/src/component/lazy.ts +51 -41
  155. package/src/create-class-decorator.ts +90 -31
  156. package/src/create-field-decorator.ts +120 -43
  157. package/src/create-getter-decorator.ts +263 -66
  158. package/src/create-lifecycle-method-decorator.ts +50 -28
  159. package/src/create-method-decorator.ts +119 -13
  160. package/src/events/command.ts +11 -9
  161. package/src/events/emit.ts +16 -22
  162. package/src/events/on-command.ts +17 -18
  163. package/src/field.ts +116 -0
  164. package/src/functions/bind.ts +4 -4
  165. package/src/functions/debounce.ts +25 -23
  166. package/src/functions/log.ts +44 -46
  167. package/src/functions/memo.ts +39 -37
  168. package/src/functions/once.ts +10 -13
  169. package/src/functions/retry.ts +19 -21
  170. package/src/functions/throttle.ts +12 -13
  171. package/src/functions/until.ts +14 -14
  172. package/src/functions/watch.ts +37 -36
  173. package/src/functions/when.ts +18 -20
  174. package/src/index.ts +12 -5
  175. package/src/lifecycle.ts +74 -0
  176. package/src/properties/compose.ts +33 -20
  177. package/src/properties/computed.ts +35 -45
  178. package/src/properties/deep-state.ts +66 -64
  179. package/src/properties/function-prop.ts +25 -27
  180. package/src/properties/history.ts +34 -35
  181. package/src/properties/index.ts +1 -0
  182. package/src/properties/persisted.ts +30 -20
  183. package/src/properties/prop.ts +30 -22
  184. package/src/properties/ref.ts +12 -10
  185. package/src/properties/resource.ts +22 -17
  186. package/src/properties/selector.ts +45 -0
  187. package/src/properties/slot.ts +28 -22
  188. package/src/properties/state.ts +32 -16
  189. package/src/properties/synced.ts +31 -19
  190. package/src/shared-method.ts +101 -0
  191. package/dist/__tests__/component.test.d.ts +0 -2
  192. package/dist/__tests__/component.test.d.ts.map +0 -1
  193. package/dist/__tests__/component.test.js +0 -147
  194. package/dist/__tests__/component.test.js.map +0 -1
  195. package/dist/__tests__/compose.test.d.ts +0 -2
  196. package/dist/__tests__/compose.test.d.ts.map +0 -1
  197. package/dist/__tests__/compose.test.js +0 -164
  198. package/dist/__tests__/compose.test.js.map +0 -1
  199. package/dist/__tests__/deep-state.test.d.ts +0 -2
  200. package/dist/__tests__/deep-state.test.d.ts.map +0 -1
  201. package/dist/__tests__/deep-state.test.js +0 -221
  202. package/dist/__tests__/deep-state.test.js.map +0 -1
  203. package/dist/__tests__/events.test.d.ts +0 -2
  204. package/dist/__tests__/events.test.d.ts.map +0 -1
  205. package/dist/__tests__/events.test.js +0 -295
  206. package/dist/__tests__/events.test.js.map +0 -1
  207. package/dist/__tests__/functions.test.d.ts +0 -2
  208. package/dist/__tests__/functions.test.d.ts.map +0 -1
  209. package/dist/__tests__/functions.test.js +0 -618
  210. package/dist/__tests__/functions.test.js.map +0 -1
  211. package/dist/__tests__/lazy.test.d.ts +0 -2
  212. package/dist/__tests__/lazy.test.d.ts.map +0 -1
  213. package/dist/__tests__/lazy.test.js +0 -227
  214. package/dist/__tests__/lazy.test.js.map +0 -1
  215. package/dist/__tests__/memo.test.d.ts +0 -2
  216. package/dist/__tests__/memo.test.d.ts.map +0 -1
  217. package/dist/__tests__/memo.test.js +0 -249
  218. package/dist/__tests__/memo.test.js.map +0 -1
  219. package/dist/__tests__/properties-browser.test.d.ts +0 -2
  220. package/dist/__tests__/properties-browser.test.d.ts.map +0 -1
  221. package/dist/__tests__/properties-browser.test.js +0 -227
  222. package/dist/__tests__/properties-browser.test.js.map +0 -1
  223. package/dist/__tests__/properties.test.d.ts +0 -2
  224. package/dist/__tests__/properties.test.d.ts.map +0 -1
  225. package/dist/__tests__/properties.test.js +0 -1253
  226. package/dist/__tests__/properties.test.js.map +0 -1
  227. package/dist/__tests__/ref.test.d.ts +0 -2
  228. package/dist/__tests__/ref.test.d.ts.map +0 -1
  229. package/dist/__tests__/ref.test.js +0 -106
  230. package/dist/__tests__/ref.test.js.map +0 -1
  231. package/dist/__tests__/resource.test.d.ts +0 -2
  232. package/dist/__tests__/resource.test.d.ts.map +0 -1
  233. package/dist/__tests__/resource.test.js +0 -217
  234. package/dist/__tests__/resource.test.js.map +0 -1
  235. package/dist/__tests__/slot.test.d.ts +0 -2
  236. package/dist/__tests__/slot.test.d.ts.map +0 -1
  237. package/dist/__tests__/slot.test.js +0 -147
  238. package/dist/__tests__/slot.test.js.map +0 -1
  239. package/dist/__tests__/synced.test.d.ts +0 -2
  240. package/dist/__tests__/synced.test.d.ts.map +0 -1
  241. package/dist/__tests__/synced.test.js +0 -161
  242. package/dist/__tests__/synced.test.js.map +0 -1
  243. package/dist/__tests__/until.test.d.ts +0 -2
  244. package/dist/__tests__/until.test.d.ts.map +0 -1
  245. package/dist/__tests__/until.test.js +0 -88
  246. package/dist/__tests__/until.test.js.map +0 -1
  247. package/dist/create-accessor-decorator.d.ts +0 -14
  248. package/dist/create-accessor-decorator.d.ts.map +0 -1
  249. package/dist/create-accessor-decorator.js +0 -27
  250. package/dist/create-accessor-decorator.js.map +0 -1
  251. package/src/__tests__/component.test.ts +0 -167
  252. package/src/__tests__/compose.test.ts +0 -202
  253. package/src/__tests__/deep-state.test.ts +0 -265
  254. package/src/__tests__/events.test.ts +0 -366
  255. package/src/__tests__/functions.test.ts +0 -803
  256. package/src/__tests__/lazy.test.ts +0 -296
  257. package/src/__tests__/memo.test.ts +0 -293
  258. package/src/__tests__/properties-browser.test.ts +0 -288
  259. package/src/__tests__/properties.test.ts +0 -1556
  260. package/src/__tests__/ref.test.ts +0 -131
  261. package/src/__tests__/resource.test.ts +0 -246
  262. package/src/__tests__/slot.test.ts +0 -179
  263. package/src/__tests__/synced.test.ts +0 -193
  264. package/src/__tests__/until.test.ts +0 -113
  265. package/src/create-accessor-decorator.ts +0 -45
  266. package/tsconfig.json +0 -9
@@ -1,803 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- import { StatefulComponent } from "@praxisjs/core";
4
-
5
- import { Bind } from "../functions/bind";
6
- import { Debounce } from "../functions/debounce";
7
- import { Log } from "../functions/log";
8
- import { Once } from "../functions/once";
9
- import { Retry } from "../functions/retry";
10
- import { Throttle } from "../functions/throttle";
11
-
12
- // Helpers to simulate TC39 decorator context
13
- function mockMethodContext(name: string) {
14
- const initializers: Array<(this: object) => void> = [];
15
- const ctx = {
16
- name,
17
- kind: "method" as const,
18
- addInitializer(fn: (this: object) => void) {
19
- initializers.push(fn);
20
- },
21
- runInitializers(instance: object) {
22
- initializers.forEach((fn) => { fn.call(instance); });
23
- },
24
- };
25
- return ctx;
26
- }
27
-
28
- // ── Once ─────────────────────────────────────────────────────────────────────
29
-
30
- describe("Once", () => {
31
- it("only calls the original function once", () => {
32
- const original = vi.fn(() => 42);
33
- const ctx = mockMethodContext("doSomething");
34
- Once()(original, ctx as unknown as ClassMethodDecoratorContext);
35
-
36
- const obj = {};
37
- ctx.runInitializers(obj);
38
- const method = (obj as Record<string, () => unknown>).doSomething;
39
-
40
- expect(method()).toBe(42);
41
- expect(method()).toBe(42);
42
- expect(original).toHaveBeenCalledTimes(1);
43
- });
44
-
45
- it("returns the same result on subsequent calls", () => {
46
- let counter = 0;
47
- const original = () => ++counter;
48
- const ctx = mockMethodContext("inc");
49
- Once()(original, ctx as unknown as ClassMethodDecoratorContext);
50
-
51
- const obj = {};
52
- ctx.runInitializers(obj);
53
- const method = (obj as Record<string, () => unknown>).inc;
54
-
55
- method();
56
- method();
57
- method();
58
- expect(counter).toBe(1);
59
- });
60
-
61
- it("is per-instance — different instances call independently", () => {
62
- const original = vi.fn(() => "result");
63
- const ctx = mockMethodContext("fn");
64
- Once()(original, ctx as unknown as ClassMethodDecoratorContext);
65
-
66
- const a = {};
67
- const b = {};
68
- ctx.runInitializers(a);
69
- ctx.runInitializers(b);
70
-
71
- const methodA = (a as Record<string, () => unknown>).fn;
72
- const methodB = (b as Record<string, () => unknown>).fn;
73
- methodA();
74
- methodB();
75
- expect(original).toHaveBeenCalledTimes(2);
76
- });
77
- });
78
-
79
- // ── Retry ─────────────────────────────────────────────────────────────────────
80
-
81
- describe("Retry", () => {
82
- it("returns result if the first attempt succeeds", async () => {
83
- const original = vi.fn(async () => "ok");
84
- const ctx = mockMethodContext("fetch");
85
- Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
86
-
87
- const obj = {};
88
- ctx.runInitializers(obj);
89
- const method = (obj as Record<string, () => Promise<unknown>>).fetch;
90
-
91
- const result = await method();
92
- expect(result).toBe("ok");
93
- expect(original).toHaveBeenCalledTimes(1);
94
- });
95
-
96
- it("retries and succeeds on a later attempt", async () => {
97
- let attempt = 0;
98
- const original = vi.fn(async () => {
99
- attempt++;
100
- if (attempt < 3) throw new Error("fail");
101
- return "success";
102
- });
103
- const ctx = mockMethodContext("call");
104
- Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
105
-
106
- const obj = {};
107
- ctx.runInitializers(obj);
108
- const method = (obj as Record<string, () => Promise<unknown>>).call;
109
-
110
- const result = await method();
111
- expect(result).toBe("success");
112
- expect(original).toHaveBeenCalledTimes(3);
113
- });
114
-
115
- it("throws after exhausting all attempts", async () => {
116
- const original = vi.fn(async () => {
117
- throw new Error("always fails");
118
- });
119
- const ctx = mockMethodContext("bad");
120
- Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
121
-
122
- const obj = {};
123
- ctx.runInitializers(obj);
124
- const method = (obj as Record<string, () => Promise<unknown>>).bad;
125
-
126
- await expect(method()).rejects.toThrow("always fails");
127
- expect(original).toHaveBeenCalledTimes(3);
128
- });
129
-
130
- it("applies delay+backoff between retries", async () => {
131
- vi.useFakeTimers();
132
- const original = vi.fn(async () => { throw new Error("fail"); });
133
- const ctx = mockMethodContext("delayed");
134
- Retry(3, { delay: 100, backoff: 2 })(
135
- original,
136
- ctx as unknown as ClassMethodDecoratorContext,
137
- );
138
-
139
- const obj = {};
140
- ctx.runInitializers(obj);
141
- const method = (obj as Record<string, () => Promise<unknown>>).delayed;
142
-
143
- const p = method();
144
- const rejection = expect(p).rejects.toThrow("fail");
145
- await vi.runAllTimersAsync();
146
- await rejection;
147
- expect(original).toHaveBeenCalledTimes(3);
148
- vi.useRealTimers();
149
- });
150
-
151
- it("calls onRetry callback for each failed attempt (except last)", async () => {
152
- const onRetry = vi.fn();
153
- const original = vi.fn(async () => {
154
- throw new Error("err");
155
- });
156
- const ctx = mockMethodContext("op");
157
- Retry(3, { onRetry })(original, ctx as unknown as ClassMethodDecoratorContext);
158
-
159
- const obj = {};
160
- ctx.runInitializers(obj);
161
- const method = (obj as Record<string, () => Promise<unknown>>).op;
162
-
163
- await expect(method()).rejects.toThrow();
164
- expect(onRetry).toHaveBeenCalledTimes(2); // attempts 1 and 2, not the last
165
- });
166
-
167
- it("maxAttempts=1 fails immediately without retrying", async () => {
168
- const original = vi.fn(async () => { throw new Error("instant fail"); });
169
- const ctx = mockMethodContext("once");
170
- Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
171
-
172
- const obj = {};
173
- ctx.runInitializers(obj);
174
- const method = (obj as Record<string, () => Promise<unknown>>).once;
175
-
176
- await expect(method()).rejects.toThrow("instant fail");
177
- expect(original).toHaveBeenCalledTimes(1);
178
- });
179
-
180
- it("non-Error throw is wrapped in an Error", async () => {
181
- const original = vi.fn(async () => {
182
- // eslint-disable-next-line @typescript-eslint/only-throw-error
183
- throw "string error";
184
- });
185
- const ctx = mockMethodContext("strThrow");
186
- Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
187
-
188
- const obj = {};
189
- ctx.runInitializers(obj);
190
- const method = (obj as Record<string, () => Promise<unknown>>).strThrow;
191
-
192
- await expect(method()).rejects.toThrow("string error");
193
- });
194
-
195
- it("maxAttempts=0 throws immediately without calling the function", async () => {
196
- const original = vi.fn(async () => "ok");
197
- const ctx = mockMethodContext("never");
198
- Retry(0)(original, ctx as unknown as ClassMethodDecoratorContext);
199
-
200
- const obj = {};
201
- ctx.runInitializers(obj);
202
- const method = (obj as Record<string, () => Promise<unknown>>).never;
203
-
204
- await expect(method()).rejects.toThrow("Unknown error");
205
- expect(original).not.toHaveBeenCalled();
206
- });
207
-
208
- it("onRetry receives the correct attempt number", async () => {
209
- const attempts: number[] = [];
210
- const original = vi.fn(async () => { throw new Error("fail"); });
211
- const ctx = mockMethodContext("numbered");
212
- Retry(3, {
213
- onRetry: (_err, attempt) => { attempts.push(attempt); },
214
- })(original, ctx as unknown as ClassMethodDecoratorContext);
215
-
216
- const obj = {};
217
- ctx.runInitializers(obj);
218
- const method = (obj as Record<string, () => Promise<unknown>>).numbered;
219
-
220
- await expect(method()).rejects.toThrow();
221
- expect(attempts).toEqual([1, 2]); // attempt 1 failed → onRetry(1), attempt 2 failed → onRetry(2)
222
- });
223
-
224
- it("backoff multiplies delay correctly", async () => {
225
- vi.useFakeTimers();
226
- const callTimes: number[] = [];
227
- const original = vi.fn(async () => {
228
- callTimes.push(Date.now());
229
- throw new Error("fail");
230
- });
231
- const ctx = mockMethodContext("backoffTest");
232
- Retry(3, { delay: 100, backoff: 2 })(
233
- original,
234
- ctx as unknown as ClassMethodDecoratorContext,
235
- );
236
-
237
- const obj = {};
238
- ctx.runInitializers(obj);
239
- const method = (obj as Record<string, () => Promise<unknown>>).backoffTest;
240
-
241
- const p = method();
242
- const rejection = expect(p).rejects.toThrow("fail");
243
- await vi.runAllTimersAsync();
244
- await rejection;
245
-
246
- expect(callTimes.length).toBe(3);
247
- // Attempt 2 runs 100ms after attempt 1
248
- expect(callTimes[1] - callTimes[0]).toBeGreaterThanOrEqual(100);
249
- // Attempt 3 runs 200ms after attempt 2 (backoff × 2)
250
- expect(callTimes[2] - callTimes[1]).toBeGreaterThanOrEqual(200);
251
- vi.useRealTimers();
252
- });
253
- });
254
-
255
- // ── Log ──────────────────────────────────────────────────────────────────────
256
-
257
- describe("Log", () => {
258
- it("calls the original function and returns its value", () => {
259
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
260
- const original = vi.fn(() => 99);
261
- const ctx = mockMethodContext("myMethod");
262
- Log({ args: false, result: false })(
263
- original,
264
- ctx as unknown as ClassMethodDecoratorContext,
265
- );
266
-
267
- class Dummy {}
268
- const dummy = new Dummy();
269
- ctx.runInitializers(dummy);
270
- const method = (dummy as Record<string, () => unknown>).myMethod;
271
-
272
- const result = method();
273
- expect(result).toBe(99);
274
- spy.mockRestore();
275
- });
276
-
277
- it("logs args when args=true", () => {
278
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
279
- const original = vi.fn((x: unknown) => x);
280
- const ctx = mockMethodContext("greet");
281
- Log({ args: true, result: false })(
282
- original,
283
- ctx as unknown as ClassMethodDecoratorContext,
284
- );
285
-
286
- class Foo {}
287
- const foo = new Foo();
288
- ctx.runInitializers(foo);
289
- const method = (foo as Record<string, (...a: unknown[]) => unknown>).greet;
290
-
291
- method("hello");
292
- expect(spy).toHaveBeenCalledWith(expect.stringContaining("args:"), ["hello"]);
293
- spy.mockRestore();
294
- });
295
-
296
- it("does not log in production when devOnly=true", () => {
297
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
298
- const original = vi.fn(() => "value");
299
- const ctx = mockMethodContext("prod");
300
- Log({ devOnly: true })(
301
- original,
302
- ctx as unknown as ClassMethodDecoratorContext,
303
- );
304
-
305
- const prevEnv = process.env.NODE_ENV;
306
- process.env.NODE_ENV = "production";
307
-
308
- class App {}
309
- const app = new App();
310
- ctx.runInitializers(app);
311
- const method = (app as Record<string, () => unknown>).prod;
312
-
313
- method();
314
- expect(spy).not.toHaveBeenCalled();
315
-
316
- process.env.NODE_ENV = prevEnv;
317
- spy.mockRestore();
318
- });
319
-
320
- it("handles async functions — logs resolved value", async () => {
321
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
322
- const original = vi.fn(async () => "async-result");
323
- const ctx = mockMethodContext("load");
324
- Log({ args: false, result: true })(
325
- original,
326
- ctx as unknown as ClassMethodDecoratorContext,
327
- );
328
-
329
- class Svc {}
330
- const svc = new Svc();
331
- ctx.runInitializers(svc);
332
- const method = (svc as Record<string, () => Promise<unknown>>).load;
333
-
334
- await method();
335
- expect(spy).toHaveBeenCalledWith(
336
- expect.stringContaining("resolved:"),
337
- "async-result",
338
- expect.any(String),
339
- );
340
- spy.mockRestore();
341
- });
342
-
343
- it("handles async rejection — logs rejected value and re-throws", async () => {
344
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
345
- const original = vi.fn(async () => { throw new Error("async-error"); });
346
- const ctx = mockMethodContext("failing");
347
- Log({ args: false, result: true })(
348
- original,
349
- ctx as unknown as ClassMethodDecoratorContext,
350
- );
351
-
352
- class Svc {}
353
- const svc = new Svc();
354
- ctx.runInitializers(svc);
355
- const method = (svc as Record<string, () => Promise<unknown>>).failing;
356
-
357
- await expect(method()).rejects.toThrow("async-error");
358
- expect(spy).toHaveBeenCalledWith(
359
- expect.stringContaining("rejected:"),
360
- expect.any(Error),
361
- );
362
- spy.mockRestore();
363
- });
364
- });
365
-
366
- // ── Log (additional branches) ─────────────────────────────────────────────────
367
-
368
- describe("Log — time option", () => {
369
- it("includes elapsed time in sync result log when time=true", () => {
370
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
371
- const original = vi.fn(() => "value");
372
- const ctx = mockMethodContext("timed");
373
- Log({ args: false, result: true, time: true })(
374
- original,
375
- ctx as unknown as ClassMethodDecoratorContext,
376
- );
377
-
378
- class Svc {}
379
- const svc = new Svc();
380
- ctx.runInitializers(svc);
381
- const method = (svc as Record<string, () => unknown>).timed;
382
-
383
- method();
384
- // Third argument should be a string containing "ms"
385
- expect(spy).toHaveBeenCalledWith(
386
- expect.stringContaining("returned:"),
387
- "value",
388
- expect.stringContaining("ms"),
389
- );
390
- spy.mockRestore();
391
- });
392
-
393
- it("includes elapsed time in async result log when time=true", async () => {
394
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
395
- const original = vi.fn(async () => "async-val");
396
- const ctx = mockMethodContext("asyncTimed");
397
- Log({ args: false, result: true, time: true })(
398
- original,
399
- ctx as unknown as ClassMethodDecoratorContext,
400
- );
401
-
402
- class Svc {}
403
- const svc = new Svc();
404
- ctx.runInitializers(svc);
405
- const method = (svc as Record<string, () => Promise<unknown>>).asyncTimed;
406
-
407
- await method();
408
- expect(spy).toHaveBeenCalledWith(
409
- expect.stringContaining("resolved:"),
410
- "async-val",
411
- expect.stringContaining("ms"),
412
- );
413
- spy.mockRestore();
414
- });
415
-
416
- it("skips result log when result=false", () => {
417
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
418
- const original = vi.fn(() => 42);
419
- const ctx = mockMethodContext("noResult");
420
- Log({ args: false, result: false })(
421
- original,
422
- ctx as unknown as ClassMethodDecoratorContext,
423
- );
424
-
425
- class Svc {}
426
- const svc = new Svc();
427
- ctx.runInitializers(svc);
428
- const method = (svc as Record<string, () => unknown>).noResult;
429
-
430
- method();
431
- // Should not log a "returned:" line
432
- const calls = spy.mock.calls.flat().join(" ");
433
- expect(calls).not.toContain("returned:");
434
- spy.mockRestore();
435
- });
436
-
437
- it("skips resolved log when result=false for async functions", async () => {
438
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
439
- const original = vi.fn(async () => "async-value");
440
- const ctx = mockMethodContext("asyncNoResult");
441
- Log({ args: false, result: false })(
442
- original,
443
- ctx as unknown as ClassMethodDecoratorContext,
444
- );
445
-
446
- class Svc {}
447
- const svc = new Svc();
448
- ctx.runInitializers(svc);
449
- const method = (svc as Record<string, () => Promise<unknown>>).asyncNoResult;
450
-
451
- const result = await method();
452
- expect(result).toBe("async-value");
453
- const calls = spy.mock.calls.flat().join(" ");
454
- expect(calls).not.toContain("resolved:");
455
- spy.mockRestore();
456
- });
457
-
458
- it("uses 'Unknown' as class name when constructor.name is undefined", () => {
459
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
460
- const original = vi.fn(() => "val");
461
- const ctx = mockMethodContext("m");
462
- Log({ args: false, result: true })(
463
- original,
464
- ctx as unknown as ClassMethodDecoratorContext,
465
- );
466
-
467
- // Construct an object whose constructor.name is undefined to trigger ?? "Unknown"
468
- const fakeInstance = Object.assign(Object.create(null) as object, {
469
- constructor: { name: undefined },
470
- });
471
- ctx.runInitializers(fakeInstance);
472
- const method = (fakeInstance as Record<string, () => unknown>).m;
473
-
474
- method();
475
- expect(spy).toHaveBeenCalledWith(
476
- expect.stringContaining("Unknown"),
477
- expect.anything(),
478
- expect.anything(),
479
- );
480
- spy.mockRestore();
481
- });
482
- });
483
-
484
- // ── Bind ─────────────────────────────────────────────────────────────────────
485
-
486
- describe("Bind", () => {
487
- it("binds method to the instance so 'this' is preserved", () => {
488
- class Counter {
489
- value = 10;
490
- getValue() {
491
- return this.value;
492
- }
493
- }
494
- const ctx = mockMethodContext("getValue");
495
- Bind()(Counter.prototype.getValue, ctx as unknown as ClassMethodDecoratorContext);
496
-
497
- const c = new Counter();
498
- ctx.runInitializers(c);
499
-
500
- const detached = (c as unknown as Record<string, () => number>).getValue;
501
- expect(detached()).toBe(10);
502
- });
503
- });
504
-
505
- // ── Debounce ─────────────────────────────────────────────────────────────────
506
-
507
- describe("Debounce", () => {
508
- it("defers execution until after the wait period", () => {
509
- vi.useFakeTimers();
510
- const fn = vi.fn();
511
- const ctx = mockMethodContext("search");
512
- Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
513
-
514
- const obj = {};
515
- ctx.runInitializers(obj);
516
- const method = (obj as Record<string, (...a: unknown[]) => void>).search;
517
-
518
- method("a");
519
- method("b");
520
- method("c");
521
- expect(fn).not.toHaveBeenCalled();
522
-
523
- vi.advanceTimersByTime(100);
524
- expect(fn).toHaveBeenCalledTimes(1);
525
- expect(fn).toHaveBeenCalledWith("c");
526
- vi.useRealTimers();
527
- });
528
-
529
- it("resets the timer on each call", () => {
530
- vi.useFakeTimers();
531
- const fn = vi.fn();
532
- const ctx = mockMethodContext("onInput");
533
- Debounce(50)(fn, ctx as unknown as ClassMethodDecoratorContext);
534
-
535
- const obj = {};
536
- ctx.runInitializers(obj);
537
- const method = (obj as Record<string, (...a: unknown[]) => void>).onInput;
538
-
539
- method("x");
540
- vi.advanceTimersByTime(30);
541
- method("y");
542
- vi.advanceTimersByTime(30); // 30ms since last call — not enough
543
- expect(fn).not.toHaveBeenCalled();
544
-
545
- vi.advanceTimersByTime(20); // now 50ms since "y"
546
- expect(fn).toHaveBeenCalledOnce();
547
- vi.useRealTimers();
548
- });
549
-
550
- it("multiple instances have independent timers", () => {
551
- vi.useFakeTimers();
552
- const fn = vi.fn();
553
- const ctx = mockMethodContext("act");
554
- Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
555
-
556
- const obj1 = {};
557
- const obj2 = {};
558
- ctx.runInitializers(obj1);
559
- ctx.runInitializers(obj2);
560
-
561
- const m1 = (obj1 as Record<string, (...a: unknown[]) => void>).act;
562
- const m2 = (obj2 as Record<string, (...a: unknown[]) => void>).act;
563
-
564
- m1("from-1");
565
- m2("from-2");
566
-
567
- vi.advanceTimersByTime(100);
568
- // Both timers fire independently
569
- expect(fn).toHaveBeenCalledTimes(2);
570
- expect(fn).toHaveBeenCalledWith("from-1");
571
- expect(fn).toHaveBeenCalledWith("from-2");
572
- vi.useRealTimers();
573
- });
574
-
575
- it("delay=0 fires after the next macrotask", () => {
576
- vi.useFakeTimers();
577
- const fn = vi.fn();
578
- const ctx = mockMethodContext("instant");
579
- Debounce(0)(fn, ctx as unknown as ClassMethodDecoratorContext);
580
-
581
- const obj = {};
582
- ctx.runInitializers(obj);
583
- const method = (obj as Record<string, (...a: unknown[]) => void>).instant;
584
-
585
- method("a");
586
- expect(fn).not.toHaveBeenCalled();
587
- vi.advanceTimersByTime(0);
588
- expect(fn).toHaveBeenCalledTimes(1);
589
- expect(fn).toHaveBeenCalledWith("a");
590
- vi.useRealTimers();
591
- });
592
-
593
- it("timer is cleared if onUnmount fires before debounce completes", () => {
594
- vi.useFakeTimers();
595
- const fn = vi.fn();
596
- const ctx = mockMethodContext("debounced");
597
-
598
- class TestComp extends StatefulComponent {
599
- render() { return null; }
600
- }
601
- Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
602
-
603
- const instance = new TestComp();
604
- ctx.runInitializers(instance);
605
- const method = (instance as unknown as Record<string, (...a: unknown[]) => void>).debounced;
606
-
607
- method("hello");
608
- // Unmount before the 100ms debounce fires
609
- instance.onUnmount?.();
610
-
611
- vi.advanceTimersByTime(200);
612
- expect(fn).not.toHaveBeenCalled();
613
- vi.useRealTimers();
614
- });
615
-
616
- it("onUnmount with no pending timer is a no-op (if timer !== undefined false branch)", () => {
617
- vi.useFakeTimers();
618
- const fn = vi.fn();
619
- const ctx = mockMethodContext("noTimerOp");
620
- Debounce(50)(fn, ctx as unknown as ClassMethodDecoratorContext);
621
-
622
- class BareComp extends StatefulComponent {
623
- render() { return null; }
624
- }
625
- const instance = new BareComp();
626
- ctx.runInitializers(instance);
627
-
628
- // Never call the decorated method, so timer is undefined
629
- // onUnmount with timer === undefined → false branch of if (timer !== undefined)
630
- expect(() => instance.onUnmount?.()).not.toThrow();
631
- vi.useRealTimers();
632
- });
633
-
634
- it("calls the pre-existing onUnmount when the component already had one", () => {
635
- vi.useFakeTimers();
636
- const existingUnmount = vi.fn();
637
- const fn = vi.fn();
638
- const ctx = mockMethodContext("debouncedWithPrior");
639
-
640
- class CompWithUnmount extends StatefulComponent {
641
- onUnmount() { existingUnmount(); }
642
- render() { return null; }
643
- }
644
- Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
645
-
646
- const instance = new CompWithUnmount();
647
- ctx.runInitializers(instance);
648
- const method = (instance as unknown as Record<string, (...a: unknown[]) => void>).debouncedWithPrior;
649
-
650
- method("x");
651
- instance.onUnmount?.();
652
-
653
- expect(existingUnmount).toHaveBeenCalledOnce();
654
- vi.advanceTimersByTime(200);
655
- expect(fn).not.toHaveBeenCalled(); // timer was cancelled
656
- vi.useRealTimers();
657
- });
658
-
659
- it("original method is NOT called after unmount if timer was pending", () => {
660
- vi.useFakeTimers();
661
- const fn = vi.fn();
662
- const ctx = mockMethodContext("debouncedOp");
663
-
664
- class TestComp2 extends StatefulComponent {
665
- render() { return null; }
666
- }
667
- Debounce(50)(fn, ctx as unknown as ClassMethodDecoratorContext);
668
-
669
- const instance = new TestComp2();
670
- ctx.runInitializers(instance);
671
- const method = (instance as unknown as Record<string, (...a: unknown[]) => void>).debouncedOp;
672
-
673
- method("arg1");
674
- method("arg2");
675
- instance.onUnmount?.();
676
-
677
- vi.runAllTimers();
678
- expect(fn).not.toHaveBeenCalled();
679
- vi.useRealTimers();
680
- });
681
- });
682
-
683
- // ── Throttle ─────────────────────────────────────────────────────────────────
684
-
685
- describe("Throttle", () => {
686
- it("executes the first call immediately", () => {
687
- vi.useFakeTimers();
688
- const fn = vi.fn(() => "first");
689
- const ctx = mockMethodContext("save");
690
- Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
691
-
692
- const obj = {};
693
- ctx.runInitializers(obj);
694
- const method = (obj as Record<string, (...a: unknown[]) => void>).save;
695
-
696
- method();
697
- expect(fn).toHaveBeenCalledTimes(1);
698
- vi.useRealTimers();
699
- });
700
-
701
- it("ignores calls within the throttle window", () => {
702
- vi.useFakeTimers();
703
- const fn = vi.fn();
704
- const ctx = mockMethodContext("scroll");
705
- Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
706
-
707
- const obj = {};
708
- ctx.runInitializers(obj);
709
- const method = (obj as Record<string, (...a: unknown[]) => void>).scroll;
710
-
711
- method();
712
- method();
713
- method();
714
- expect(fn).toHaveBeenCalledTimes(1);
715
- vi.useRealTimers();
716
- });
717
-
718
- it("allows a new call after the window expires", () => {
719
- vi.useFakeTimers();
720
- const fn = vi.fn();
721
- const ctx = mockMethodContext("resize");
722
- Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
723
-
724
- const obj = {};
725
- ctx.runInitializers(obj);
726
- const method = (obj as Record<string, (...a: unknown[]) => void>).resize;
727
-
728
- method();
729
- vi.advanceTimersByTime(100);
730
- method();
731
- expect(fn).toHaveBeenCalledTimes(2);
732
- vi.useRealTimers();
733
- });
734
-
735
- it("passes the first call's arguments to the underlying function", () => {
736
- vi.useFakeTimers();
737
- const fn = vi.fn();
738
- const ctx = mockMethodContext("click");
739
- Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
740
-
741
- const obj = {};
742
- ctx.runInitializers(obj);
743
- const method = (obj as Record<string, (...a: unknown[]) => void>).click;
744
-
745
- method("first-arg");
746
- method("second-arg");
747
- expect(fn).toHaveBeenCalledWith("first-arg");
748
- vi.useRealTimers();
749
- });
750
-
751
- it("multiple instances throttle independently", () => {
752
- vi.useFakeTimers();
753
- const fn = vi.fn();
754
- const ctx = mockMethodContext("ping");
755
- Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
756
-
757
- const obj1 = {};
758
- const obj2 = {};
759
- ctx.runInitializers(obj1);
760
- ctx.runInitializers(obj2);
761
-
762
- const m1 = (obj1 as Record<string, (...a: unknown[]) => void>).ping;
763
- const m2 = (obj2 as Record<string, (...a: unknown[]) => void>).ping;
764
-
765
- m1("a");
766
- m2("b");
767
- // Both first calls go through
768
- expect(fn).toHaveBeenCalledTimes(2);
769
- vi.useRealTimers();
770
- });
771
-
772
- it("negative delay is treated as 0 — first call always executes", () => {
773
- vi.useFakeTimers();
774
- const fn = vi.fn();
775
- const ctx = mockMethodContext("negDelay");
776
- Throttle(-100)(fn, ctx as unknown as ClassMethodDecoratorContext);
777
-
778
- const obj = {};
779
- ctx.runInitializers(obj);
780
- const method = (obj as Record<string, (...a: unknown[]) => void>).negDelay;
781
-
782
- method();
783
- expect(fn).toHaveBeenCalledTimes(1);
784
- vi.useRealTimers();
785
- });
786
-
787
- it("call at exactly the ms boundary executes again", () => {
788
- vi.useFakeTimers();
789
- const fn = vi.fn();
790
- const ctx = mockMethodContext("boundary");
791
- Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
792
-
793
- const obj = {};
794
- ctx.runInitializers(obj);
795
- const method = (obj as Record<string, (...a: unknown[]) => void>).boundary;
796
-
797
- method(); // first call at t=0
798
- vi.advanceTimersByTime(100); // advance to exactly 100ms
799
- method(); // second call at t=100 — should pass (now - last === ms, not less than ms)
800
- expect(fn).toHaveBeenCalledTimes(2);
801
- vi.useRealTimers();
802
- });
803
- });