@praxisjs/decorators 1.7.0 → 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 (266) hide show
  1. package/CHANGELOG.md +33 -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,9 +1,9 @@
1
1
  import type { StatefulComponent } from "@praxisjs/core";
2
- import { effect, batch } from "@praxisjs/core/internal";
2
+ import { batch, createEffect } from "@praxisjs/core/internal";
3
3
  import type { Computed } from "@praxisjs/shared";
4
4
  import { isComputed } from "@praxisjs/shared/internal";
5
5
 
6
- import { createLifecycleMethodDecorator } from "../create-lifecycle-method-decorator";
6
+ import { lifecycleMethodDecorator } from "../create-lifecycle-method-decorator";
7
7
 
8
8
  type BaseComponentKeys = keyof StatefulComponent;
9
9
 
@@ -58,43 +58,44 @@ export function Watch<
58
58
  >(...propNames: ValidateKeys<T, Keys>) {
59
59
  const props = propNames as unknown as string[];
60
60
 
61
- return createLifecycleMethodDecorator<T>({
62
- register(callback, instance) {
61
+ if (props.length === 1) {
62
+ const key = props[0];
63
+ return lifecycleMethodDecorator<T>((instance, method) => {
63
64
  const inst = instance as T & Record<string, unknown>;
65
+ let oldVal = readValue(inst, key);
66
+ return createEffect(() => {
67
+ const newVal = readValue(inst, key);
68
+ if (!Object.is(newVal, oldVal)) {
69
+ method.call(inst, newVal, oldVal);
70
+ oldVal = newVal;
71
+ }
72
+ });
73
+ });
74
+ }
64
75
 
65
- if (props.length === 1) {
66
- let oldVal = readValue(inst, props[0]);
67
- return effect(() => {
68
- const newVal = readValue(inst, props[0]);
69
- if (!Object.is(newVal, oldVal)) {
70
- callback(newVal, oldVal);
71
- oldVal = newVal;
72
- }
73
- });
74
- }
75
-
76
- const read = () =>
77
- Object.fromEntries(props.map((p) => [p, readValue(inst, p)]));
78
- let oldVals = read();
79
- let latestVals = oldVals;
80
- let scheduled = false;
81
- return effect(() => {
82
- const newVals = read();
83
- if (props.some((p) => !Object.is(newVals[p], oldVals[p]))) {
84
- latestVals = newVals;
85
- if (!scheduled) {
86
- scheduled = true;
87
- const prevVals = oldVals;
88
- queueMicrotask(() => {
89
- scheduled = false;
90
- oldVals = latestVals;
91
- batch(() => {
92
- callback(latestVals, prevVals);
93
- });
76
+ return lifecycleMethodDecorator<T>((instance, method) => {
77
+ const inst = instance as T & Record<string, unknown>;
78
+ const read = () =>
79
+ Object.fromEntries(props.map((p) => [p, readValue(inst, p)]));
80
+ let oldVals = read();
81
+ let latestVals = oldVals;
82
+ let scheduled = false;
83
+ return createEffect(() => {
84
+ const newVals = read();
85
+ if (props.some((p) => !Object.is(newVals[p], oldVals[p]))) {
86
+ latestVals = newVals;
87
+ if (!scheduled) {
88
+ scheduled = true;
89
+ const prevVals = oldVals;
90
+ queueMicrotask(() => {
91
+ scheduled = false;
92
+ oldVals = latestVals;
93
+ batch(() => {
94
+ method.call(inst, latestVals, prevVals);
94
95
  });
95
- }
96
+ });
96
97
  }
97
- });
98
- },
98
+ }
99
+ });
99
100
  });
100
101
  }
@@ -1,8 +1,8 @@
1
- import { effect } from "@praxisjs/core/internal";
1
+ import { createEffect } from "@praxisjs/core/internal";
2
2
  import type { Computed, Signal } from "@praxisjs/shared";
3
3
  import { isComputed, isSignal } from "@praxisjs/shared/internal";
4
4
 
5
- import { createLifecycleMethodDecorator } from "../create-lifecycle-method-decorator";
5
+ import { lifecycleMethodDecorator } from "../create-lifecycle-method-decorator";
6
6
 
7
7
  /** Read the current value of a property, unwrapping Signal or Computed if needed. */
8
8
  function readProp(instance: Record<string, unknown>, key: string): unknown {
@@ -16,26 +16,24 @@ export function When<T = unknown>(
16
16
  propName: string,
17
17
  condition?: (value: NonNullable<T>) => boolean,
18
18
  ) {
19
- return createLifecycleMethodDecorator({
20
- register(callback, instance) {
21
- const inst = instance as unknown as Record<string, unknown>;
22
- let fired = false;
19
+ return lifecycleMethodDecorator<object>((instance, method) => {
20
+ const inst = instance as Record<string, unknown>;
21
+ let fired = false;
23
22
 
24
- return effect(() => {
25
- if (fired) return;
26
- // Reading via readProp subscribes this effect to the underlying signal,
27
- // whether the property is a raw Signal/Computed or an @State getter.
28
- const val = readProp(inst, propName) as T;
23
+ return createEffect(() => {
24
+ if (fired) return;
25
+ // Reading via readProp subscribes this effect to the underlying signal,
26
+ // whether the property is a raw Signal/Computed or an @State getter.
27
+ const val = readProp(inst, propName) as T;
29
28
 
30
- const meetsCondition = condition
31
- ? val != null && condition(val)
32
- : val != null && Boolean(val);
29
+ const meetsCondition = condition
30
+ ? val != null && condition(val)
31
+ : val != null && Boolean(val);
33
32
 
34
- if (meetsCondition) {
35
- fired = true;
36
- callback(val as NonNullable<T>);
37
- }
38
- });
39
- },
33
+ if (meetsCondition) {
34
+ fired = true;
35
+ method.call(inst, val as NonNullable<T>);
36
+ }
37
+ });
40
38
  });
41
39
  }
package/src/index.ts CHANGED
@@ -34,12 +34,17 @@ export {
34
34
  DeepState,
35
35
  Ref,
36
36
  createRef,
37
+ Selector,
38
+ type SelectorOptions,
37
39
  } from "./properties";
38
40
  export { Command, createCommand, Emit, OnCommand } from "./events";
41
+ export { addClaimableInitializer, assertFieldContext, type FieldDecorator } from "./field";
39
42
  export {
40
43
  createFieldDecorator,
41
44
  type FieldBehavior,
42
45
  type FieldBinding,
46
+ type FieldSetup,
47
+ type SharedFieldBehavior,
43
48
  } from "./create-field-decorator";
44
49
  export { reactiveHostType, type ReactiveHost } from "./reactive-host";
45
50
  export {
@@ -50,6 +55,9 @@ export {
50
55
  export {
51
56
  createMethodDecorator,
52
57
  type MethodBehavior,
58
+ type MethodDecorator,
59
+ type MethodSetup,
60
+ type SharedMethodBehavior,
53
61
  } from "./create-method-decorator";
54
62
  export {
55
63
  createLifecycleMethodDecorator,
@@ -58,14 +66,13 @@ export {
58
66
  export {
59
67
  createGetterDecorator,
60
68
  type GetterBehavior,
69
+ type GetterDecorator,
70
+ type GetterSetup,
71
+ type SharedGetterBehavior,
61
72
  createWritableGetterDecorator,
62
73
  type WritableGetterBehavior,
63
74
  type WritableGetterBinding,
75
+ type SharedWritableGetterBehavior,
64
76
  createGetterObserverDecorator,
65
77
  type GetterObserverBehavior,
66
78
  } from "./create-getter-decorator";
67
- export {
68
- createAccessorDecorator,
69
- type AccessorBehavior,
70
- type AccessorBinding,
71
- } from "./create-accessor-decorator";
@@ -0,0 +1,74 @@
1
+ import { disposeEffect, type EffectNode } from "@praxisjs/core/internal";
2
+
3
+ export type LifecycleHook = (instance: object) => void;
4
+
5
+ export interface LifecycleHost {
6
+ onMount?: ((this: object) => void) | undefined;
7
+ onUnmount?: ((this: object) => void) | undefined;
8
+ }
9
+
10
+ /** What a mount registration leaves to undo on unmount: a stop function or an effect to dispose. */
11
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
12
+ export type LifecycleCleanup = (() => void) | EffectNode | undefined | void;
13
+
14
+ /**
15
+ * Runs `mount` / `unmount` (called with the instance) after the instance's current `onMount` /
16
+ * `onUnmount`, and so after the hooks added before them. Calling `instance.onMount()` by hand runs
17
+ * them too.
18
+ *
19
+ * The previous hooks are called with `this` rather than bound to the instance: two closures per
20
+ * hook pair are all this allocates.
21
+ */
22
+ export function addLifecycleHooks(
23
+ object: object,
24
+ mount: LifecycleHook | undefined,
25
+ unmount: LifecycleHook | undefined,
26
+ ): void {
27
+ const instance = object as LifecycleHost;
28
+ const prevMount = instance.onMount;
29
+ const prevUnmount = instance.onUnmount;
30
+ instance.onMount = function (this: object) {
31
+ prevMount?.call(this);
32
+ mount?.(this);
33
+ };
34
+ instance.onUnmount = function (this: object) {
35
+ prevUnmount?.call(this);
36
+ unmount?.(this);
37
+ };
38
+ }
39
+
40
+ function stop(cleanup: LifecycleCleanup): void {
41
+ if (typeof cleanup === "function") cleanup();
42
+ else if (cleanup != null) disposeEffect(cleanup);
43
+ }
44
+
45
+ /**
46
+ * Calls `start(instance, arg)` on every mount and undoes what it returned on unmount. Like
47
+ * `addLifecycleHooks`, it chains onto the instance's hooks with two closures and no bound
48
+ * functions; the cleanup lives in their shared scope.
49
+ */
50
+ export function addMountRegistration<T extends object, A>(
51
+ object: T,
52
+ start: (instance: T, arg: A) => LifecycleCleanup,
53
+ arg: A,
54
+ ): void {
55
+ const instance = object as LifecycleHost;
56
+ const prevMount = instance.onMount;
57
+ const prevUnmount = instance.onUnmount;
58
+ let cleanup: LifecycleCleanup;
59
+ instance.onMount = function (this: object) {
60
+ prevMount?.call(this);
61
+ // A second onMount() without an onUnmount() in between would otherwise leak the first
62
+ // registration, which nothing could stop anymore.
63
+ const previous = cleanup;
64
+ cleanup = undefined;
65
+ stop(previous);
66
+ cleanup = start(this as T, arg);
67
+ };
68
+ instance.onUnmount = function (this: object) {
69
+ prevUnmount?.call(this);
70
+ const previous = cleanup;
71
+ cleanup = undefined;
72
+ stop(previous);
73
+ };
74
+ }
@@ -1,6 +1,7 @@
1
- import type { StatefulComponent, Composable } from "@praxisjs/core";
1
+ import type { Composable } from "@praxisjs/core";
2
2
 
3
- import { createFieldDecorator } from "../create-field-decorator";
3
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
4
+ import { addLifecycleHooks } from "../lifecycle";
4
5
 
5
6
  interface ComposeFactory { __isComposeFactory: true; resolve: (instance: object) => unknown }
6
7
 
@@ -43,24 +44,36 @@ export function Compose(
43
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
45
  ComposableClass: new (...args: any[]) => Composable,
45
46
  ...ctorArgs: unknown[]
46
- ) {
47
- return createFieldDecorator({
48
- bind(instance: StatefulComponent, _name: string, _initialValue: unknown) {
49
- const resolvedArgs = ctorArgs.map((arg) => {
50
- if (isFactory(arg)) return arg.resolve(instance);
51
- if (typeof arg !== "string") return arg;
52
- const prop = (instance as unknown as Record<string, unknown>)[arg];
53
- return prop !== undefined ? prop : arg;
54
- });
55
-
56
- const composable = new ComposableClass(...resolvedArgs);
57
- const view = buildView(composable.setup());
47
+ ): FieldDecorator {
48
+ return sharedField((name) => {
49
+ const composableSlot = Symbol(`praxis.composable.${name}`);
50
+ const viewSlot = Symbol(`praxis.compose.${name}`);
51
+ const composableOf = (instance: object) => (instance as FieldHost)[composableSlot] as Composable;
52
+ const mount = (instance: object) => { composableOf(instance).onMount?.(); };
53
+ const unmount = (instance: object) => { composableOf(instance).onUnmount?.(); };
54
+ return {
55
+ descriptor: {
56
+ get(this: FieldHost) {
57
+ return this[viewSlot];
58
+ },
59
+ },
60
+ init(instance) {
61
+ const resolvedArgs = ctorArgs.map((arg) => {
62
+ if (isFactory(arg)) return arg.resolve(instance);
63
+ if (typeof arg !== "string") return arg;
64
+ const prop = instance[arg];
65
+ return prop !== undefined ? prop : arg;
66
+ });
58
67
 
59
- return {
60
- descriptor: { get: () => view },
61
- onMount: composable.onMount?.bind(composable),
62
- onUnmount: composable.onUnmount?.bind(composable),
63
- };
64
- },
68
+ const composable = new ComposableClass(...resolvedArgs);
69
+ instance[composableSlot] = composable;
70
+ instance[viewSlot] = buildView(composable.setup());
71
+ const hasMount = typeof composable.onMount === "function";
72
+ const hasUnmount = typeof composable.onUnmount === "function";
73
+ if (hasMount || hasUnmount) {
74
+ addLifecycleHooks(instance, hasMount ? mount : undefined, hasUnmount ? unmount : undefined);
75
+ }
76
+ },
77
+ };
65
78
  });
66
79
  }
@@ -1,55 +1,45 @@
1
- import { computed } from "@praxisjs/core/internal";
1
+ import { ComputedNode, readComputed } from "@praxisjs/core/internal";
2
2
 
3
- import { createAccessorDecorator } from "../create-accessor-decorator";
4
- import { createWritableGetterDecorator } from "../create-getter-decorator";
3
+ import { createGetterDecorator, type GetterDecorator } from "../create-getter-decorator";
5
4
 
6
- export interface ComputedOptions<T = unknown> {
7
- get?: (this: object) => T;
8
- set?: (this: object, value: T) => void;
5
+ export interface ComputedOptions {
6
+ /**
7
+ * Handles writes to the property. A `set` accessor declared next to the getter in the class
8
+ * does the same, and TypeScript sees it.
9
+ */
10
+ set?(this: object, value: unknown): void;
9
11
  }
10
12
 
11
- // Accessor form — @Computed({ get, set }) on an `accessor` field.
12
- // TypeScript sees the accessor as writable; no cast needed when assigning.
13
- // The `Owner` generic is inferred from the `this` type of the provided functions.
14
- export function Computed<T, Owner extends object = object>(options: {
15
- get: (this: Owner) => T;
16
- set: (this: Owner, value: T) => void;
17
- }): <This extends object>(
18
- target: ClassAccessorDecoratorTarget<This, T>,
19
- context: ClassAccessorDecoratorContext<This, T>,
20
- ) => ClassAccessorDecoratorResult<This, T>;
13
+ type Host = Record<symbol, ComputedNode | undefined>;
21
14
 
22
- // Getter form — @Computed() or @Computed({ set }) on a `get` accessor.
23
- export function Computed(options?: {
24
- set?: (this: object, value: unknown) => void;
25
- }): <This, R>(
26
- target: (this: This) => R,
27
- context: ClassGetterDecoratorContext<This, R>,
28
- ) => (this: This) => R;
29
-
30
- export function Computed<T = unknown>(options?: ComputedOptions<T>): unknown {
31
- const optionsGet = options?.get as ((this: object) => unknown) | undefined;
32
- const optionsSet = options?.set as ((this: object, value: unknown) => void) | undefined;
33
-
34
- if (optionsGet) {
35
- return createAccessorDecorator({
36
- bind(instance, _name, _initialValue) {
37
- const c = computed(() => optionsGet.call(instance));
38
- return {
39
- get: c,
40
- set: optionsSet ? (value) => { optionsSet.call(instance, value); } : undefined,
41
- };
42
- },
43
- });
44
- }
15
+ // Kept out of the getter: a closure created inside it would make every read allocate a context.
16
+ function computedFor<T>(instance: T, getter: (this: T) => unknown): ComputedNode {
17
+ return new ComputedNode(() => getter.call(instance));
18
+ }
45
19
 
46
- return createWritableGetterDecorator({
47
- bind(instance, _name, original) {
48
- const c = computed(() => original.call(instance));
20
+ /**
21
+ * Caches a getter as a reactive computed. A `set` accessor declared next to the getter makes the
22
+ * property writable.
23
+ */
24
+ export function Computed(options?: ComputedOptions): GetterDecorator {
25
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- installed on the class, which calls it with the instance as `this`
26
+ const set = options?.set;
27
+ return createGetterDecorator<Host>({
28
+ label: "@Computed()",
29
+ setup(original, context) {
30
+ const node = Symbol(`praxis.computed.${String(context.name)}`);
49
31
  return {
50
- get: c,
51
- set: optionsSet ? (value) => { optionsSet.call(instance, value); } : undefined,
32
+ // Shared by every instance; the computed itself is only created the first time it is read,
33
+ // so a getter that a component never reads costs nothing beyond its slot.
34
+ get(this: Host) {
35
+ return readComputed((this[node] ??= computedFor(this, original)));
36
+ },
37
+ set,
38
+ init(instance) {
39
+ // The slot is created up front so every instance keeps the same shape.
40
+ instance[node] = undefined;
41
+ },
52
42
  };
53
43
  },
54
- });
44
+ }) as GetterDecorator;
55
45
  }
@@ -1,75 +1,77 @@
1
- import { markStateDirty, signal } from "@praxisjs/core/internal";
1
+ import { markStateDirty, readSignal, SignalNode, writeSignal } from "@praxisjs/core/internal";
2
2
 
3
- import { createFieldDecorator } from "../create-field-decorator";
3
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
4
4
 
5
5
  import type { ReactiveHost } from "../reactive-host";
6
6
 
7
- // One proxy per raw object: repeated reads return the same reference (so `a.b === a.b` and
8
- // identity comparisons in effects hold) and don't allocate a new Proxy on every access.
9
- function deepProxy<T extends object>(
10
- target: T,
11
- notify: () => void,
12
- cache: WeakMap<object, object>,
13
- ): T {
14
- const existing = cache.get(target);
15
- if (existing) return existing as T;
7
+ // One per decorated field per instance. It is also the handler of every proxy it hands out, so
8
+ // proxying a nested object allocates only the Proxy itself.
9
+ class DeepStateCell implements ProxyHandler<object> {
10
+ readonly version = new SignalNode(0);
11
+ // One proxy per raw object: repeated reads return the same reference (so `a.b === a.b` and
12
+ // identity comparisons in effects hold) and don't allocate a new Proxy on every access.
13
+ private proxies: WeakMap<object, object> | undefined = undefined;
14
+ current: unknown;
16
15
 
17
- const proxy = new Proxy(target, {
18
- get(obj, key, receiver) {
19
- const val: unknown = Reflect.get(obj, key, receiver);
20
- if (val !== null && typeof val === "object") {
21
- return deepProxy(val, notify, cache);
22
- }
23
- return val;
24
- },
25
- set(obj, key, value) {
26
- const had = Object.hasOwn(obj, key);
27
- const prev: unknown = Reflect.get(obj, key);
28
- const result = Reflect.set(obj, key, value);
29
- if (!had || !Object.is(prev, value)) notify();
30
- return result;
31
- },
32
- deleteProperty(obj, key) {
33
- const had = Object.hasOwn(obj, key);
34
- const result = Reflect.deleteProperty(obj, key);
35
- if (had) notify();
36
- return result;
37
- },
38
- });
39
- cache.set(target, proxy);
40
- return proxy;
41
- }
16
+ constructor(private readonly host: object, value: unknown) {
17
+ this.current = this.wrap(value);
18
+ }
19
+
20
+ wrap(value: unknown): unknown {
21
+ if (value === null || typeof value !== "object") return value;
22
+ const proxies = (this.proxies ??= new WeakMap());
23
+ let proxy = proxies.get(value);
24
+ if (proxy === undefined) {
25
+ proxy = new Proxy(value, this);
26
+ proxies.set(value, proxy);
27
+ }
28
+ return proxy;
29
+ }
30
+
31
+ changed(): void {
32
+ markStateDirty(this.host);
33
+ writeSignal(this.version, this.version.value + 1);
34
+ }
42
35
 
43
- export function DeepState() {
44
- return createFieldDecorator<ReactiveHost>({
45
- bind(instance, _name, initialValue) {
46
- const version = signal(0);
47
- const cache = new WeakMap<object, object>();
48
- let proxy =
49
- initialValue !== null && typeof initialValue === "object"
50
- ? deepProxy(initialValue, notify, cache)
51
- : initialValue;
36
+ get(obj: object, key: string | symbol, receiver: unknown): unknown {
37
+ return this.wrap(Reflect.get(obj, key, receiver));
38
+ }
52
39
 
53
- function notify() {
54
- markStateDirty(instance);
55
- version.update((v) => v + 1);
56
- }
40
+ set(obj: object, key: string | symbol, value: unknown): boolean {
41
+ const had = Object.hasOwn(obj, key);
42
+ const prev: unknown = Reflect.get(obj, key);
43
+ const result = Reflect.set(obj, key, value);
44
+ if (!had || !Object.is(prev, value)) this.changed();
45
+ return result;
46
+ }
57
47
 
58
- return {
59
- descriptor: {
60
- get() {
61
- version();
62
- return proxy;
63
- },
64
- set(value: unknown) {
65
- proxy =
66
- value !== null && typeof value === "object"
67
- ? deepProxy(value, notify, cache)
68
- : value;
69
- notify();
70
- },
48
+ deleteProperty(obj: object, key: string | symbol): boolean {
49
+ const had = Object.hasOwn(obj, key);
50
+ const result = Reflect.deleteProperty(obj, key);
51
+ if (had) this.changed();
52
+ return result;
53
+ }
54
+ }
55
+
56
+ export function DeepState(): FieldDecorator<ReactiveHost> {
57
+ return sharedField<ReactiveHost>((name) => {
58
+ const slot = Symbol(`praxis.deepState.${name}`);
59
+ return {
60
+ descriptor: {
61
+ get(this: FieldHost) {
62
+ const cell = this[slot] as DeepStateCell;
63
+ readSignal(cell.version);
64
+ return cell.current;
65
+ },
66
+ set(this: FieldHost, value: unknown) {
67
+ const cell = this[slot] as DeepStateCell;
68
+ cell.current = cell.wrap(value);
69
+ cell.changed();
71
70
  },
72
- };
73
- },
71
+ },
72
+ init(instance, initialValue) {
73
+ instance[slot] = new DeepStateCell(instance, initialValue);
74
+ },
75
+ };
74
76
  });
75
77
  }
@@ -4,34 +4,32 @@ import {
4
4
  setComponentDefault,
5
5
  } from "@praxisjs/core/internal";
6
6
 
7
- import { createFieldDecorator } from "../create-field-decorator";
7
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
8
8
 
9
- export function FunctionProp() {
10
- return createFieldDecorator({
11
- bind(instance, name, initialValue) {
9
+ export function FunctionProp(): FieldDecorator {
10
+ return sharedField((name) => ({
11
+ descriptor: {
12
+ get(this: FieldHost) {
13
+ const fromParent = getComponentRawProp(this, name);
14
+ if (fromParent !== undefined) {
15
+ if (
16
+ process.env.NODE_ENV !== "production" &&
17
+ typeof fromParent !== "function"
18
+ ) {
19
+ console.warn(
20
+ `[FunctionProp] "${name}" received a non-function value (${typeof fromParent}). @FunctionProp() expects a function.`,
21
+ );
22
+ }
23
+ return fromParent;
24
+ }
25
+ return getComponentDefault(this, name);
26
+ },
27
+ set(this: FieldHost, value: unknown) {
28
+ setComponentDefault(this, name, value);
29
+ },
30
+ },
31
+ init(instance, initialValue) {
12
32
  setComponentDefault(instance, name, initialValue);
13
- return {
14
- descriptor: {
15
- get() {
16
- const fromParent = getComponentRawProp(instance, name);
17
- if (fromParent !== undefined) {
18
- if (
19
- process.env.NODE_ENV !== "production" &&
20
- typeof fromParent !== "function"
21
- ) {
22
- console.warn(
23
- `[FunctionProp] "${name}" received a non-function value (${typeof fromParent}). @FunctionProp() expects a function.`,
24
- );
25
- }
26
- return fromParent;
27
- }
28
- return getComponentDefault(instance, name);
29
- },
30
- set(value: unknown) {
31
- setComponentDefault(instance, name, value);
32
- },
33
- },
34
- };
35
33
  },
36
- });
34
+ }));
37
35
  }