@praxisjs/decorators 1.6.1 → 2.0.0

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 (270) hide show
  1. package/CHANGELOG.md +46 -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/index.d.ts +1 -1
  55. package/dist/functions/index.d.ts.map +1 -1
  56. package/dist/functions/index.js.map +1 -1
  57. package/dist/functions/log.d.ts +2 -1
  58. package/dist/functions/log.d.ts.map +1 -1
  59. package/dist/functions/log.js +38 -40
  60. package/dist/functions/log.js.map +1 -1
  61. package/dist/functions/memo.d.ts +9 -1
  62. package/dist/functions/memo.d.ts.map +1 -1
  63. package/dist/functions/memo.js +44 -30
  64. package/dist/functions/memo.js.map +1 -1
  65. package/dist/functions/once.d.ts +2 -1
  66. package/dist/functions/once.d.ts.map +1 -1
  67. package/dist/functions/once.js +10 -14
  68. package/dist/functions/once.js.map +1 -1
  69. package/dist/functions/retry.d.ts +2 -1
  70. package/dist/functions/retry.d.ts.map +1 -1
  71. package/dist/functions/retry.js +21 -23
  72. package/dist/functions/retry.js.map +1 -1
  73. package/dist/functions/throttle.d.ts +2 -1
  74. package/dist/functions/throttle.d.ts.map +1 -1
  75. package/dist/functions/throttle.js +12 -14
  76. package/dist/functions/throttle.js.map +1 -1
  77. package/dist/functions/until.d.ts +2 -1
  78. package/dist/functions/until.d.ts.map +1 -1
  79. package/dist/functions/until.js +13 -14
  80. package/dist/functions/until.js.map +1 -1
  81. package/dist/functions/watch.d.ts +1 -1
  82. package/dist/functions/watch.d.ts.map +1 -1
  83. package/dist/functions/watch.js +36 -34
  84. package/dist/functions/watch.js.map +1 -1
  85. package/dist/functions/when.d.ts +1 -1
  86. package/dist/functions/when.d.ts.map +1 -1
  87. package/dist/functions/when.js +19 -21
  88. package/dist/functions/when.js.map +1 -1
  89. package/dist/index.d.ts +6 -6
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +2 -2
  92. package/dist/index.js.map +1 -1
  93. package/dist/lifecycle.d.ts +24 -0
  94. package/dist/lifecycle.d.ts.map +1 -0
  95. package/dist/lifecycle.js +55 -0
  96. package/dist/lifecycle.js.map +1 -0
  97. package/dist/properties/compose.d.ts +3 -2
  98. package/dist/properties/compose.d.ts.map +1 -1
  99. package/dist/properties/compose.js +33 -19
  100. package/dist/properties/compose.js.map +1 -1
  101. package/dist/properties/computed.d.ts +12 -10
  102. package/dist/properties/computed.d.ts.map +1 -1
  103. package/dist/properties/computed.js +26 -21
  104. package/dist/properties/computed.js.map +1 -1
  105. package/dist/properties/deep-state.d.ts +2 -1
  106. package/dist/properties/deep-state.d.ts.map +1 -1
  107. package/dist/properties/deep-state.js +66 -49
  108. package/dist/properties/deep-state.js.map +1 -1
  109. package/dist/properties/function-prop.d.ts +2 -1
  110. package/dist/properties/function-prop.d.ts.map +1 -1
  111. package/dist/properties/function-prop.js +20 -22
  112. package/dist/properties/function-prop.js.map +1 -1
  113. package/dist/properties/history.d.ts +2 -2
  114. package/dist/properties/history.d.ts.map +1 -1
  115. package/dist/properties/history.js +34 -31
  116. package/dist/properties/history.js.map +1 -1
  117. package/dist/properties/index.d.ts +1 -0
  118. package/dist/properties/index.d.ts.map +1 -1
  119. package/dist/properties/index.js +1 -0
  120. package/dist/properties/index.js.map +1 -1
  121. package/dist/properties/persisted.d.ts +2 -1
  122. package/dist/properties/persisted.d.ts.map +1 -1
  123. package/dist/properties/persisted.js +30 -19
  124. package/dist/properties/persisted.js.map +1 -1
  125. package/dist/properties/prop.d.ts +2 -1
  126. package/dist/properties/prop.d.ts.map +1 -1
  127. package/dist/properties/prop.js +26 -22
  128. package/dist/properties/prop.js.map +1 -1
  129. package/dist/properties/ref.d.ts +2 -1
  130. package/dist/properties/ref.d.ts.map +1 -1
  131. package/dist/properties/ref.js +11 -8
  132. package/dist/properties/ref.js.map +1 -1
  133. package/dist/properties/resource.d.ts.map +1 -1
  134. package/dist/properties/resource.js +21 -16
  135. package/dist/properties/resource.js.map +1 -1
  136. package/dist/properties/selector.d.ts +12 -0
  137. package/dist/properties/selector.d.ts.map +1 -0
  138. package/dist/properties/selector.js +25 -0
  139. package/dist/properties/selector.js.map +1 -0
  140. package/dist/properties/slot.d.ts +2 -1
  141. package/dist/properties/slot.d.ts.map +1 -1
  142. package/dist/properties/slot.js +22 -20
  143. package/dist/properties/slot.js.map +1 -1
  144. package/dist/properties/state.d.ts.map +1 -1
  145. package/dist/properties/state.js +27 -16
  146. package/dist/properties/state.js.map +1 -1
  147. package/dist/properties/synced.d.ts +2 -1
  148. package/dist/properties/synced.d.ts.map +1 -1
  149. package/dist/properties/synced.js +27 -19
  150. package/dist/properties/synced.js.map +1 -1
  151. package/dist/shared-method.d.ts +14 -0
  152. package/dist/shared-method.d.ts.map +1 -0
  153. package/dist/shared-method.js +87 -0
  154. package/dist/shared-method.js.map +1 -0
  155. package/package.json +14 -5
  156. package/src/component/component.ts +8 -27
  157. package/src/component/lazy.ts +51 -41
  158. package/src/create-class-decorator.ts +90 -31
  159. package/src/create-field-decorator.ts +120 -43
  160. package/src/create-getter-decorator.ts +263 -66
  161. package/src/create-lifecycle-method-decorator.ts +50 -28
  162. package/src/create-method-decorator.ts +119 -13
  163. package/src/events/command.ts +11 -9
  164. package/src/events/emit.ts +16 -22
  165. package/src/events/on-command.ts +17 -18
  166. package/src/field.ts +116 -0
  167. package/src/functions/bind.ts +4 -4
  168. package/src/functions/debounce.ts +25 -23
  169. package/src/functions/index.ts +1 -1
  170. package/src/functions/log.ts +44 -46
  171. package/src/functions/memo.ts +54 -28
  172. package/src/functions/once.ts +10 -13
  173. package/src/functions/retry.ts +19 -21
  174. package/src/functions/throttle.ts +12 -13
  175. package/src/functions/until.ts +14 -14
  176. package/src/functions/watch.ts +37 -36
  177. package/src/functions/when.ts +18 -20
  178. package/src/index.ts +13 -5
  179. package/src/lifecycle.ts +74 -0
  180. package/src/properties/compose.ts +33 -20
  181. package/src/properties/computed.ts +35 -45
  182. package/src/properties/deep-state.ts +68 -53
  183. package/src/properties/function-prop.ts +25 -27
  184. package/src/properties/history.ts +34 -33
  185. package/src/properties/index.ts +1 -0
  186. package/src/properties/persisted.ts +41 -27
  187. package/src/properties/prop.ts +30 -22
  188. package/src/properties/ref.ts +12 -10
  189. package/src/properties/resource.ts +22 -17
  190. package/src/properties/selector.ts +45 -0
  191. package/src/properties/slot.ts +28 -22
  192. package/src/properties/state.ts +32 -16
  193. package/src/properties/synced.ts +31 -19
  194. package/src/shared-method.ts +101 -0
  195. package/dist/__tests__/component.test.d.ts +0 -2
  196. package/dist/__tests__/component.test.d.ts.map +0 -1
  197. package/dist/__tests__/component.test.js +0 -147
  198. package/dist/__tests__/component.test.js.map +0 -1
  199. package/dist/__tests__/compose.test.d.ts +0 -2
  200. package/dist/__tests__/compose.test.d.ts.map +0 -1
  201. package/dist/__tests__/compose.test.js +0 -164
  202. package/dist/__tests__/compose.test.js.map +0 -1
  203. package/dist/__tests__/deep-state.test.d.ts +0 -2
  204. package/dist/__tests__/deep-state.test.d.ts.map +0 -1
  205. package/dist/__tests__/deep-state.test.js +0 -150
  206. package/dist/__tests__/deep-state.test.js.map +0 -1
  207. package/dist/__tests__/events.test.d.ts +0 -2
  208. package/dist/__tests__/events.test.d.ts.map +0 -1
  209. package/dist/__tests__/events.test.js +0 -295
  210. package/dist/__tests__/events.test.js.map +0 -1
  211. package/dist/__tests__/functions.test.d.ts +0 -2
  212. package/dist/__tests__/functions.test.d.ts.map +0 -1
  213. package/dist/__tests__/functions.test.js +0 -618
  214. package/dist/__tests__/functions.test.js.map +0 -1
  215. package/dist/__tests__/lazy.test.d.ts +0 -2
  216. package/dist/__tests__/lazy.test.d.ts.map +0 -1
  217. package/dist/__tests__/lazy.test.js +0 -227
  218. package/dist/__tests__/lazy.test.js.map +0 -1
  219. package/dist/__tests__/memo.test.d.ts +0 -2
  220. package/dist/__tests__/memo.test.d.ts.map +0 -1
  221. package/dist/__tests__/memo.test.js +0 -168
  222. package/dist/__tests__/memo.test.js.map +0 -1
  223. package/dist/__tests__/properties-browser.test.d.ts +0 -2
  224. package/dist/__tests__/properties-browser.test.d.ts.map +0 -1
  225. package/dist/__tests__/properties-browser.test.js +0 -178
  226. package/dist/__tests__/properties-browser.test.js.map +0 -1
  227. package/dist/__tests__/properties.test.d.ts +0 -2
  228. package/dist/__tests__/properties.test.d.ts.map +0 -1
  229. package/dist/__tests__/properties.test.js +0 -1226
  230. package/dist/__tests__/properties.test.js.map +0 -1
  231. package/dist/__tests__/ref.test.d.ts +0 -2
  232. package/dist/__tests__/ref.test.d.ts.map +0 -1
  233. package/dist/__tests__/ref.test.js +0 -106
  234. package/dist/__tests__/ref.test.js.map +0 -1
  235. package/dist/__tests__/resource.test.d.ts +0 -2
  236. package/dist/__tests__/resource.test.d.ts.map +0 -1
  237. package/dist/__tests__/resource.test.js +0 -217
  238. package/dist/__tests__/resource.test.js.map +0 -1
  239. package/dist/__tests__/slot.test.d.ts +0 -2
  240. package/dist/__tests__/slot.test.d.ts.map +0 -1
  241. package/dist/__tests__/slot.test.js +0 -147
  242. package/dist/__tests__/slot.test.js.map +0 -1
  243. package/dist/__tests__/synced.test.d.ts +0 -2
  244. package/dist/__tests__/synced.test.d.ts.map +0 -1
  245. package/dist/__tests__/synced.test.js +0 -161
  246. package/dist/__tests__/synced.test.js.map +0 -1
  247. package/dist/__tests__/until.test.d.ts +0 -2
  248. package/dist/__tests__/until.test.d.ts.map +0 -1
  249. package/dist/__tests__/until.test.js +0 -88
  250. package/dist/__tests__/until.test.js.map +0 -1
  251. package/dist/create-accessor-decorator.d.ts +0 -14
  252. package/dist/create-accessor-decorator.d.ts.map +0 -1
  253. package/dist/create-accessor-decorator.js +0 -27
  254. package/dist/create-accessor-decorator.js.map +0 -1
  255. package/src/__tests__/component.test.ts +0 -167
  256. package/src/__tests__/compose.test.ts +0 -202
  257. package/src/__tests__/deep-state.test.ts +0 -185
  258. package/src/__tests__/events.test.ts +0 -366
  259. package/src/__tests__/functions.test.ts +0 -803
  260. package/src/__tests__/lazy.test.ts +0 -296
  261. package/src/__tests__/memo.test.ts +0 -208
  262. package/src/__tests__/properties-browser.test.ts +0 -234
  263. package/src/__tests__/properties.test.ts +0 -1526
  264. package/src/__tests__/ref.test.ts +0 -131
  265. package/src/__tests__/resource.test.ts +0 -246
  266. package/src/__tests__/slot.test.ts +0 -179
  267. package/src/__tests__/synced.test.ts +0 -193
  268. package/src/__tests__/until.test.ts +0 -113
  269. package/src/create-accessor-decorator.ts +0 -45
  270. package/tsconfig.json +0 -9
@@ -1,26 +1,56 @@
1
- export interface GetterBehavior {
1
+ import { assertMemberKind, describeValue } from "./field";
2
+ import { addLifecycleHooks, addMountRegistration, type LifecycleCleanup } from "./lifecycle";
3
+
4
+ type Getter<This = object, Value = unknown> = (this: This) => Value;
5
+
6
+ /** A getter decorator usable on getters of classes extending `Host` whose type is assignable to `Value`. */
7
+ export type GetterDecorator<Host = object, Value = unknown> = <This extends Host, V extends Value>(
8
+ target: (this: This) => V,
9
+ context: ClassGetterDecoratorContext<This, V>,
10
+ ) => (this: This) => V;
11
+
12
+ /**
13
+ * What a shared getter decorator installs: `get` (the original getter when absent) and `set` are
14
+ * shared by every instance (`this` is the instance), so per-instance state belongs in symbol slots
15
+ * created in `init`.
16
+ */
17
+ export interface GetterSetup<This = object, Value = unknown> {
18
+ get?(this: This): Value;
19
+ set?(this: This, value: Value): void;
20
+ /** Runs once per instance, before the class's fields are initialized. */
21
+ init?: (instance: This) => void;
22
+ onMount?: (instance: This) => void;
23
+ onUnmount?: (instance: This) => void;
24
+ }
25
+
26
+ export interface SharedGetterBehavior<Host = object, Value = unknown> {
27
+ /** Names the decorator in error messages, e.g. `"@Clamp()"`. */
28
+ label?: string;
29
+ /**
30
+ * Runs once per decorated getter, when the class is defined: returns the replacement getter, or
31
+ * a `GetterSetup`.
32
+ */
33
+ setup(
34
+ original: Getter<Host, Value>,
35
+ context: ClassGetterDecoratorContext<Host, Value>,
36
+ ): Getter<Host, Value> | GetterSetup<Host, Value>;
37
+ }
38
+
39
+ export interface SharedWritableGetterBehavior<Host = object, Value = unknown> {
40
+ /** Names the decorator in error messages, e.g. `"@Overridable()"`. */
41
+ label?: string;
42
+ /** Runs once per decorated getter, when the class is defined. */
43
+ setup(original: Getter<Host, Value>, context: ClassGetterDecoratorContext<Host, Value>): GetterSetup<Host, Value>;
44
+ }
45
+
46
+ export interface GetterBehavior<T = object> {
47
+ /** Names the decorator in error messages. */
48
+ label?: string;
2
49
  /**
3
50
  * Given the original getter and the current instance, returns a function
4
51
  * that produces the getter value. Called on every property access.
5
52
  */
6
- wrap(
7
- original: (this: object) => unknown,
8
- instance: object,
9
- ): () => unknown;
10
- }
11
-
12
- export function createGetterDecorator(behavior: GetterBehavior) {
13
- return function <This, T>(
14
- target: (this: This) => T,
15
- _context: ClassGetterDecoratorContext<This, T>,
16
- ): (this: This) => T {
17
- return function (this: This): T {
18
- return behavior.wrap(
19
- target as (this: object) => unknown,
20
- this as object,
21
- )() as T;
22
- };
23
- };
53
+ wrap(original: (this: object) => unknown, instance: T): () => unknown;
24
54
  }
25
55
 
26
56
  export interface WritableGetterBinding {
@@ -28,64 +58,231 @@ export interface WritableGetterBinding {
28
58
  set?: (value: unknown) => void;
29
59
  }
30
60
 
31
- export interface WritableGetterBehavior {
61
+ export interface WritableGetterBehavior<T = object> {
62
+ /** Names the decorator in error messages. */
63
+ label?: string;
64
+ /**
65
+ * Called once per instance during construction. Returns the get/set pair the property reads and
66
+ * writes through for that instance. If `set` is omitted, assigning the property throws a
67
+ * `TypeError`, as for a getter without a setter.
68
+ */
69
+ bind(instance: T, name: string | symbol, original: (this: object) => unknown): WritableGetterBinding;
70
+ }
71
+
72
+ export interface GetterObserverBehavior<T = object, Value = unknown> {
73
+ /** Names the decorator in error messages. */
74
+ label?: string;
32
75
  /**
33
- * Called once per instance during construction. Returns the get/set pair
34
- * that will be installed on the instance via `Object.defineProperty`.
35
- * If `set` is omitted, the property is read-only at runtime (though TypeScript
36
- * may still see a setter if one is declared in the class body).
76
+ * Called once per instance, while it is built and before its fields are initialized. Use it to
77
+ * set up observation without changing the getter's value. A function it returns runs when the
78
+ * instance unmounts.
37
79
  */
38
- bind(
39
- instance: object,
40
- name: string | symbol,
41
- original: (this: object) => unknown,
42
- ): WritableGetterBinding;
43
- }
44
-
45
- export function createWritableGetterDecorator(behavior: WritableGetterBehavior) {
46
- return function <This, T>(
47
- target: (this: This) => T,
48
- context: ClassGetterDecoratorContext<This, T>,
49
- ): (this: This) => T {
80
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
81
+ observe?(getter: (this: object) => Value, instance: T, name: string): (() => void) | undefined | void;
82
+ /**
83
+ * Called on every mount of each instance, once its fields are set. What it returns (a stop
84
+ * function or an effect) is undone on unmount.
85
+ */
86
+ register?(getter: (this: object) => Value, instance: T, name: string): LifecycleCleanup;
87
+ }
88
+
89
+ // Replacement getter -> the getter it wraps. Lets a setter added by an inner decorator find the
90
+ // prototype entry after outer getter decorators replaced its getter again.
91
+ const wraps = new WeakMap<Getter, Getter>();
92
+
93
+ function isFrom(get: Getter | undefined, target: Getter): boolean {
94
+ for (let g = get; g !== undefined; g = wraps.get(g)) if (g === target) return true;
95
+ return false;
96
+ }
97
+
98
+ /**
99
+ * Adds `set` to the class entry of the getter `get`: on the prototype (the class itself for a
100
+ * static getter) where it lives, found from the first instance. Returns false when the getter was
101
+ * not found, i.e. a decorator applied by hand, and defines the pair on `target` alone instead.
102
+ */
103
+ // A property descriptor, read as data: its functions are only passed along, never called here.
104
+ interface FoundDescriptor {
105
+ get?: Getter;
106
+ enumerable?: boolean;
107
+ }
108
+
109
+ function addSetter(
110
+ target: object,
111
+ context: ClassGetterDecoratorContext,
112
+ get: Getter,
113
+ set: NonNullable<PropertyDescriptor["set"]>,
114
+ ): boolean {
115
+ const name = context.name;
116
+ let fallback: FoundDescriptor | undefined;
117
+ for (
118
+ let owner = (context.static ? target : Object.getPrototypeOf(target)) as object | null;
119
+ owner !== null;
120
+ owner = Object.getPrototypeOf(owner) as object | null
121
+ ) {
122
+ const descriptor = Object.getOwnPropertyDescriptor(owner, name) as FoundDescriptor | undefined;
123
+ if (descriptor === undefined) continue;
124
+ if (isFrom(descriptor.get, get)) {
125
+ Object.defineProperty(owner, name, { get: descriptor.get, set, enumerable: descriptor.enumerable, configurable: true });
126
+ return true;
127
+ }
128
+ fallback ??= descriptor;
129
+ }
130
+ Object.defineProperty(target, name, { get: fallback?.get ?? get, set, enumerable: false, configurable: true });
131
+ return false;
132
+ }
133
+
134
+ interface SharedGetterLike {
135
+ label?: string;
136
+ setup(original: Getter, context: ClassGetterDecoratorContext): unknown;
137
+ }
138
+
139
+ function sharedGetter(behavior: SharedGetterLike) {
140
+ const label = behavior.label ?? "This getter decorator";
141
+ return function (target: Getter, context: ClassGetterDecoratorContext): Getter {
142
+ assertMemberKind(context, "getter", label);
143
+ const result = behavior.setup(target, context);
144
+ if (typeof result !== "object" && typeof result !== "function") {
145
+ throw new TypeError(`${label}: setup() must return a getter or a GetterSetup object, got ${describeValue(result)}.`);
146
+ }
147
+ if (result === null) {
148
+ throw new TypeError(`${label}: setup() must return a getter or a GetterSetup object, got null.`);
149
+ }
150
+ const spec = (typeof result === "function" ? { get: result } : result) as GetterSetup;
151
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- installed on the class, which calls them with the instance as `this`
152
+ const { get = target, set, init, onMount, onUnmount } = spec;
153
+ if (typeof get !== "function") {
154
+ throw new TypeError(`${label}: setup() returned a "get" that is not a function.`);
155
+ }
156
+ if (get !== target) wraps.set(get, target);
157
+ const hooks = onMount !== undefined || onUnmount !== undefined;
158
+ if (hooks && context.static) {
159
+ throw new TypeError(`${label}: onMount/onUnmount need an instance getter, not the static "${String(context.name)}".`);
160
+ }
161
+ if (set !== undefined || init !== undefined || hooks) {
162
+ let installed = set === undefined;
163
+ context.addInitializer(function (this: unknown) {
164
+ const instance = this as object;
165
+ if (!installed) installed = addSetter(instance, context, get, set as NonNullable<PropertyDescriptor["set"]>);
166
+ init?.(instance);
167
+ if (hooks) addLifecycleHooks(instance, onMount, onUnmount);
168
+ });
169
+ }
170
+ return get;
171
+ };
172
+ }
173
+
174
+ function wrapGetterDecorator(behavior: GetterBehavior) {
175
+ const label = behavior.label ?? "This getter decorator";
176
+ return function <This, T>(target: (this: This) => T, context: ClassGetterDecoratorContext<This, T>): (this: This) => T {
177
+ assertMemberKind(context, "getter", label);
178
+ const getter = function (this: This): T {
179
+ return behavior.wrap(target as (this: object) => unknown, this as object)() as T;
180
+ };
181
+ wraps.set(getter as Getter, target as Getter);
182
+ return getter;
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Builds a getter decorator that changes what a getter returns.
188
+ *
189
+ * With `setup`, the recommended form, the replacement getter is created once per decorated getter
190
+ * and shared by every instance: neither building an instance nor reading the getter allocates.
191
+ *
192
+ * With `wrap`, a function is created on every read.
193
+ */
194
+ export function createGetterDecorator<Host = object, Value = unknown>(
195
+ behavior: SharedGetterBehavior<Host, Value>,
196
+ ): GetterDecorator<Host, Value>;
197
+ export function createGetterDecorator(
198
+ behavior: GetterBehavior,
199
+ ): <This, T>(target: (this: This) => T, context: ClassGetterDecoratorContext<This, T>) => (this: This) => T;
200
+ export function createGetterDecorator(behavior: SharedGetterBehavior | GetterBehavior): unknown {
201
+ if (!("setup" in behavior)) return wrapGetterDecorator(behavior);
202
+ return sharedGetter(behavior as SharedGetterLike);
203
+ }
204
+
205
+ function bindWritableGetter(behavior: WritableGetterBehavior) {
206
+ const label = behavior.label ?? "This getter decorator";
207
+ return function <This, T>(target: (this: This) => T, context: ClassGetterDecoratorContext<This, T>): (this: This) => T {
208
+ assertMemberKind(context, "getter", label);
209
+ const name = context.name;
210
+ // Each instance's binding lives in this slot, read through one accessor pair on the class.
211
+ const slot = Symbol(`praxis.getter.${String(name)}`);
212
+ type Host = Record<symbol, WritableGetterBinding | undefined>;
213
+ const original = target as (this: object) => unknown;
214
+ const bindingOf = (instance: Host): WritableGetterBinding =>
215
+ (instance[slot] ??= behavior.bind(instance, name, original));
216
+ const get = function (this: Host): T {
217
+ return bindingOf(this).get() as T;
218
+ };
219
+ const set = function (this: Host, value: unknown): void {
220
+ const write = bindingOf(this).set;
221
+ if (write === undefined) throw new TypeError(`Cannot set property ${String(name)}, which has only a getter.`);
222
+ write(value);
223
+ };
224
+ wraps.set(get as Getter, original);
225
+ let installed = false;
50
226
  context.addInitializer(function (this: unknown) {
51
- const instance = this as object;
52
- const binding = behavior.bind(instance, context.name, target as (this: object) => unknown);
53
- const descriptor: PropertyDescriptor = {
54
- get: binding.get,
55
- configurable: true,
56
- enumerable: false,
57
- };
58
- if (binding.set !== undefined) descriptor.set = binding.set;
59
- Object.defineProperty(instance, context.name, descriptor);
227
+ const instance = this as Host;
228
+ instance[slot] = behavior.bind(instance, name, original);
229
+ if (!installed) installed = addSetter(instance, context as ClassGetterDecoratorContext, get as Getter, set);
60
230
  });
61
- return target;
231
+ return get as (this: This) => T;
62
232
  };
63
233
  }
64
234
 
65
- export interface GetterObserverBehavior {
66
- /**
67
- * Called once per instance when the class is initialized.
68
- * Use this to set up observation side-effects on the getter without
69
- * changing its return value (e.g. tracking changes for devtools).
70
- */
71
- observe(
72
- getter: (this: object) => unknown,
73
- instance: object,
74
- name: string,
75
- ): void;
235
+ /**
236
+ * Builds a getter decorator that owns writes to the property as well as reads.
237
+ *
238
+ * With `setup`, the recommended form, `get` and `set` are created once per decorated getter and
239
+ * shared by every instance.
240
+ *
241
+ * With `bind`, a get/set pair is created for each instance while it is built.
242
+ */
243
+ export function createWritableGetterDecorator<Host = object, Value = unknown>(
244
+ behavior: SharedWritableGetterBehavior<Host, Value>,
245
+ ): GetterDecorator<Host, Value>;
246
+ export function createWritableGetterDecorator(
247
+ behavior: WritableGetterBehavior,
248
+ ): <This, T>(target: (this: This) => T, context: ClassGetterDecoratorContext<This, T>) => (this: This) => T;
249
+ export function createWritableGetterDecorator(
250
+ behavior: SharedWritableGetterBehavior | WritableGetterBehavior,
251
+ ): unknown {
252
+ if (!("setup" in behavior)) return bindWritableGetter(behavior);
253
+ return sharedGetter(behavior as SharedGetterLike);
76
254
  }
77
255
 
78
- export function createGetterObserverDecorator(behavior: GetterObserverBehavior) {
79
- return function <This, T>(
80
- target: (this: This) => T,
81
- context: ClassGetterDecoratorContext<This, T>,
256
+ /**
257
+ * Builds a getter decorator that observes the getter (for logging, devtools, syncing) without
258
+ * changing its value: `observe` runs once per instance while it is built, `register` on every mount.
259
+ */
260
+ export function createGetterObserverDecorator<T = object, Value = unknown>(
261
+ behavior: GetterObserverBehavior<T, Value>,
262
+ ) {
263
+ const label = behavior.label ?? "This getter decorator";
264
+ if (behavior.observe === undefined && behavior.register === undefined) {
265
+ throw new TypeError(`${label}: createGetterObserverDecorator() needs observe() or register().`);
266
+ }
267
+ return function <This extends T, V extends Value>(
268
+ target: (this: This) => V,
269
+ context: ClassGetterDecoratorContext<This, V>,
82
270
  ): void {
271
+ assertMemberKind(context, "getter", label);
272
+ const name = context.name as string;
273
+ const getter = target as unknown as (this: object) => Value;
274
+ const observes = behavior.observe !== undefined;
275
+ const mount =
276
+ behavior.register === undefined
277
+ ? undefined
278
+ : (instance: T, read: (this: object) => Value) => behavior.register?.(read, instance, name);
83
279
  context.addInitializer(function (this: unknown) {
84
- behavior.observe(
85
- target as (this: object) => unknown,
86
- this as object,
87
- context.name as string,
88
- );
280
+ const instance = this as T & object;
281
+ if (observes) {
282
+ const cleanup = behavior.observe?.(getter, instance, name);
283
+ if (typeof cleanup === "function") addLifecycleHooks(instance, undefined, cleanup);
284
+ }
285
+ if (mount !== undefined) addMountRegistration(instance, mount, getter);
89
286
  });
90
287
  };
91
288
  }
@@ -1,39 +1,61 @@
1
1
  import type { StatefulComponent } from "@praxisjs/core";
2
2
 
3
- export interface LifecycleMethodBehavior<
4
- T extends StatefulComponent = StatefulComponent,
5
- > {
6
- register(
7
- callback: (...args: unknown[]) => void,
8
- instance: T,
9
- ): (() => void) | undefined;
3
+ import { assertMemberKind } from "./field";
4
+ import { addMountRegistration, type LifecycleCleanup } from "./lifecycle";
5
+
6
+ export type { LifecycleCleanup };
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ export interface LifecycleMethodBehavior<T extends object = StatefulComponent, Args extends unknown[] = any[]> {
10
+ /** Names the decorator in error messages, e.g. `"@OnResize()"`. */
11
+ label?: string;
12
+ /**
13
+ * Runs on every mount of each instance with the method bound to it. Returns what undoes the
14
+ * registration on unmount, if anything.
15
+ */
16
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
17
+ register(callback: (...args: Args) => void, instance: T, name: string): (() => void) | undefined | void;
10
18
  }
11
19
 
12
- export function createLifecycleMethodDecorator<
13
- T extends StatefulComponent = StatefulComponent,
14
- >(behavior: LifecycleMethodBehavior<T>) {
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ type AnyMethod = (this: any, ...args: any[]) => void;
22
+
23
+ /**
24
+ * Builds a method decorator that calls `start` on every mount and undoes what it returned on
25
+ * unmount.
26
+ */
27
+ export function lifecycleMethodDecorator<T extends object>(
28
+ start: (instance: T, method: AnyMethod, name: string) => LifecycleCleanup,
29
+ label = "This lifecycle method decorator",
30
+ ) {
15
31
  return function (
16
32
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
33
  value: (this: T, ...args: any[]) => void,
18
- _context: ClassMethodDecoratorContext<T>,
34
+ context: ClassMethodDecoratorContext<T>,
19
35
  ): void {
20
- _context.addInitializer(function (this: unknown) {
21
- const instance = this as T;
22
- const prevMount = instance.onMount?.bind(instance);
23
- const prevUnmount = instance.onUnmount?.bind(instance);
24
- let cleanup: (() => void) | undefined;
25
-
26
- instance.onMount = function (this: T) {
27
- prevMount?.call(this);
28
- const result = behavior.register(value.bind(this), this);
29
- if (result) cleanup = result;
30
- };
31
-
32
- instance.onUnmount = function (this: T) {
33
- prevUnmount?.call(this);
34
- cleanup?.();
35
- cleanup = undefined;
36
- };
36
+ assertMemberKind(context, "method", label);
37
+ if (context.static) {
38
+ throw new TypeError(`${label} decorates instance methods only, not the static method "${String(context.name)}".`);
39
+ }
40
+ const name = String(context.name);
41
+ const run = (instance: T, method: AnyMethod) => start(instance, method, name);
42
+ context.addInitializer(function (this: T) {
43
+ addMountRegistration(this, run, value);
37
44
  });
38
45
  };
39
46
  }
47
+
48
+ /**
49
+ * Builds a method decorator that registers the method somewhere on every mount (an event listener,
50
+ * a subscription) and undoes it on unmount.
51
+ */
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ export function createLifecycleMethodDecorator<T extends object = StatefulComponent, Args extends unknown[] = any[]>(
54
+ behavior: LifecycleMethodBehavior<T, Args>,
55
+ ): <This extends T>(value: (this: This, ...args: Args) => void, context: ClassMethodDecoratorContext<This>) => void {
56
+ const label = behavior.label ?? "This lifecycle method decorator";
57
+ return lifecycleMethodDecorator<T>(
58
+ (instance, method, name) => behavior.register(method.bind(instance) as (...args: Args) => void, instance, name),
59
+ label,
60
+ ) as never;
61
+ }
@@ -1,29 +1,135 @@
1
1
  import type { StatefulComponent } from "@praxisjs/core";
2
2
 
3
- export interface MethodBehavior {
3
+ import { assertMemberKind, describeValue } from "./field";
4
+ import { addLifecycleHooks } from "./lifecycle";
5
+ import { type AnyMethod, sharedMethod } from "./shared-method";
6
+
7
+ export type { AnyMethod };
8
+
9
+ export interface MethodBehavior<T = object> {
10
+ /** Names the decorator in error messages, e.g. `"@Confirm()"`. */
11
+ label?: string;
12
+ /**
13
+ * Runs once per instance, while it is built: returns the function that runs in place of the
14
+ * method for that instance.
15
+ */
4
16
  wrap(
5
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
18
  original: (...args: any[]) => any,
7
- instance: object,
19
+ instance: T,
8
20
  name: string,
9
21
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
22
  ): (...args: any[]) => any;
11
23
  }
12
24
 
13
- export function createMethodDecorator(behavior: MethodBehavior) {
25
+ /** A replacement method, with hooks that run on every mount and unmount of each instance. */
26
+ export interface MethodSetup<This = object, M extends AnyMethod = AnyMethod> {
27
+ method: M;
28
+ onMount?: (instance: This) => void;
29
+ onUnmount?: (instance: This) => void;
30
+ }
31
+
32
+ export interface SharedMethodBehavior<Host = object, M extends AnyMethod = AnyMethod> {
33
+ /** Names the decorator in error messages, e.g. `"@Confirm()"`. */
34
+ label?: string;
35
+ /**
36
+ * Runs once per decorated method, when the class is defined: returns the replacement, shared by
37
+ * every instance (`this` is the instance), or a `MethodSetup` that also has lifecycle hooks.
38
+ *
39
+ * `M` is never inferred from it, so a decorator fits every method unless `M` is given.
40
+ */
41
+ setup(
42
+ original: NoInfer<M>,
43
+ context: ClassMethodDecoratorContext<Host, NoInfer<M>>,
44
+ ): NoInfer<M> | MethodSetup<Host, NoInfer<M>>;
45
+ }
46
+
47
+ /** A method decorator usable on methods of classes extending `Host` whose type is assignable to `M`. */
48
+ export type MethodDecorator<Host = object, M extends AnyMethod = AnyMethod> = <This extends Host, V extends M>(
49
+ value: V,
50
+ context: ClassMethodDecoratorContext<This, V>,
51
+ ) => V;
52
+
53
+ const DEFAULT_LABEL = "This method decorator";
54
+
55
+ function sharedMethodDecorator<Host, M extends AnyMethod>(
56
+ behavior: SharedMethodBehavior<Host, M>,
57
+ ): MethodDecorator<Host, M> {
58
+ const label = behavior.label ?? DEFAULT_LABEL;
59
+ return function (value, context) {
60
+ assertMemberKind(context, "method", label);
61
+ const result: unknown = behavior.setup(value, context as unknown as ClassMethodDecoratorContext<Host, M>);
62
+ const setup = typeof result === "object" && result !== null ? (result as MethodSetup) : undefined;
63
+ const method: unknown = setup === undefined ? result : setup.method;
64
+ if (typeof method !== "function") {
65
+ throw new TypeError(`${label}: setup() must return a function or { method }, got ${describeValue(method)}.`);
66
+ }
67
+ if (setup !== undefined && (setup.onMount !== undefined || setup.onUnmount !== undefined)) {
68
+ if (context.static) {
69
+ throw new TypeError(`${label}: onMount/onUnmount need an instance method, not the static "${String(context.name)}".`);
70
+ }
71
+ const { onMount, onUnmount } = setup;
72
+ context.addInitializer(function () {
73
+ addLifecycleHooks(this as object, onMount, onUnmount);
74
+ });
75
+ }
76
+ return sharedMethod(context, value, method as AnyMethod) as typeof value;
77
+ };
78
+ }
79
+
80
+ function wrapMethodDecorator<T>(behavior: MethodBehavior<T>) {
81
+ const label = behavior.label ?? DEFAULT_LABEL;
14
82
  return function (
15
83
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
84
  value: (this: object, ...args: any[]) => any,
17
- context: ClassMethodDecoratorContext<StatefulComponent>,
18
- ): void {
19
- context.addInitializer(function (this: unknown) {
20
- const instance = this as object;
21
- const name = context.name as string;
22
- Object.defineProperty(instance, name, {
23
- value: behavior.wrap(value, instance, name),
24
- configurable: true,
25
- writable: true,
26
- });
85
+ context: ClassMethodDecoratorContext,
86
+ ) {
87
+ assertMemberKind(context, "method", label);
88
+ const name = context.name as string;
89
+ // Each instance's wrapper lives in this slot, called through one replacement on the prototype:
90
+ // no property definition per instance, and decorators stacked around this one keep working.
91
+ const slot = Symbol(`praxis.method.${name}`);
92
+ type Host = Record<symbol, AnyMethod | undefined>;
93
+ const wrapFor = (instance: object): AnyMethod => {
94
+ const wrapped: unknown = behavior.wrap(value, instance as T, name);
95
+ if (typeof wrapped !== "function") {
96
+ throw new TypeError(`${label}: wrap() must return a function, got ${describeValue(wrapped)}.`);
97
+ }
98
+ return wrapped as AnyMethod;
99
+ };
100
+ const method = function (this: Host, ...args: unknown[]): unknown {
101
+ // An object the constructor did not build (Object.create(prototype)) gets its wrapper here.
102
+ return (this[slot] ??= wrapFor(this)).apply(this, args) as unknown;
103
+ };
104
+ context.addInitializer(function () {
105
+ (this as Host)[slot] = wrapFor(this as object);
27
106
  });
107
+ return sharedMethod(context, value, method);
28
108
  };
29
109
  }
110
+
111
+ /**
112
+ * Builds a method decorator.
113
+ *
114
+ * With `setup`, the recommended form, the replacement is created once per decorated method and
115
+ * shared by every instance, which keeps any state in symbol slots: building an instance costs
116
+ * nothing.
117
+ *
118
+ * With `wrap`, a wrapper is created for each instance while it is built.
119
+ *
120
+ * Either way, reading the method from an instance returns it bound to that instance, the same
121
+ * function on every read, so `onClick={this.save}` works.
122
+ */
123
+ export function createMethodDecorator<Host = object, M extends AnyMethod = AnyMethod>(
124
+ behavior: SharedMethodBehavior<Host, M>,
125
+ ): MethodDecorator<Host, M>;
126
+ export function createMethodDecorator<T = object>(
127
+ behavior: MethodBehavior<T>,
128
+ ): (
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
+ value: (this: object, ...args: any[]) => any,
131
+ context: ClassMethodDecoratorContext<StatefulComponent>,
132
+ ) => void;
133
+ export function createMethodDecorator(behavior: SharedMethodBehavior | MethodBehavior): unknown {
134
+ return "setup" in behavior ? sharedMethodDecorator(behavior) : wrapMethodDecorator(behavior);
135
+ }
@@ -1,4 +1,4 @@
1
- import { createFieldDecorator } from "../create-field-decorator";
1
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
2
2
 
3
3
  export interface Command<T = void> {
4
4
  trigger(arg: T): void;
@@ -19,13 +19,15 @@ export function createCommand<T = void>(): Command<T> {
19
19
  }
20
20
 
21
21
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
22
- export function Command<T = void>() {
23
- return createFieldDecorator({
24
- bind(_instance, _name, _initialValue) {
25
- const command = createCommand<T>();
26
- return {
27
- descriptor: { get: () => command },
28
- };
29
- },
22
+ export function Command<T = void>(): FieldDecorator {
23
+ return sharedField((name) => {
24
+ const command = Symbol(`praxis.command.${name}`);
25
+ return {
26
+ descriptor: {
27
+ get(this: FieldHost) {
28
+ return (this[command] ??= createCommand<T>());
29
+ },
30
+ },
31
+ };
30
32
  });
31
33
  }