@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,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
@@ -1,6 +1,7 @@
1
1
  export { Component, Lazy } from "./component";
2
2
  export {
3
3
  Memo,
4
+ type MemoOptions,
4
5
  Bind,
5
6
  Log,
6
7
  Once,
@@ -33,12 +34,17 @@ export {
33
34
  DeepState,
34
35
  Ref,
35
36
  createRef,
37
+ Selector,
38
+ type SelectorOptions,
36
39
  } from "./properties";
37
40
  export { Command, createCommand, Emit, OnCommand } from "./events";
41
+ export { addClaimableInitializer, assertFieldContext, type FieldDecorator } from "./field";
38
42
  export {
39
43
  createFieldDecorator,
40
44
  type FieldBehavior,
41
45
  type FieldBinding,
46
+ type FieldSetup,
47
+ type SharedFieldBehavior,
42
48
  } from "./create-field-decorator";
43
49
  export { reactiveHostType, type ReactiveHost } from "./reactive-host";
44
50
  export {
@@ -49,6 +55,9 @@ export {
49
55
  export {
50
56
  createMethodDecorator,
51
57
  type MethodBehavior,
58
+ type MethodDecorator,
59
+ type MethodSetup,
60
+ type SharedMethodBehavior,
52
61
  } from "./create-method-decorator";
53
62
  export {
54
63
  createLifecycleMethodDecorator,
@@ -57,14 +66,13 @@ export {
57
66
  export {
58
67
  createGetterDecorator,
59
68
  type GetterBehavior,
69
+ type GetterDecorator,
70
+ type GetterSetup,
71
+ type SharedGetterBehavior,
60
72
  createWritableGetterDecorator,
61
73
  type WritableGetterBehavior,
62
74
  type WritableGetterBinding,
75
+ type SharedWritableGetterBehavior,
63
76
  createGetterObserverDecorator,
64
77
  type GetterObserverBehavior,
65
78
  } from "./create-getter-decorator";
66
- export {
67
- createAccessorDecorator,
68
- type AccessorBehavior,
69
- type AccessorBinding,
70
- } 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,62 +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
- function deepProxy<T extends object>(target: T, notify: () => void): T {
8
- return new Proxy(target, {
9
- get(obj, key, receiver) {
10
- const val: unknown = Reflect.get(obj, key, receiver);
11
- if (val !== null && typeof val === "object") {
12
- return deepProxy(val, notify);
13
- }
14
- return val;
15
- },
16
- set(obj, key, value) {
17
- const result = Reflect.set(obj, key, value);
18
- notify();
19
- return result;
20
- },
21
- deleteProperty(obj, key) {
22
- const result = Reflect.deleteProperty(obj, key);
23
- notify();
24
- return result;
25
- },
26
- });
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;
15
+
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
+ }
35
+
36
+ get(obj: object, key: string | symbol, receiver: unknown): unknown {
37
+ return this.wrap(Reflect.get(obj, key, receiver));
38
+ }
39
+
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
+ }
47
+
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
+ }
27
54
  }
28
55
 
29
- export function DeepState() {
30
- return createFieldDecorator<ReactiveHost>({
31
- bind(instance, _name, initialValue) {
32
- const version = signal(0);
33
- let current = initialValue;
34
- let proxy =
35
- current !== null && typeof current === "object"
36
- ? deepProxy(current, notify)
37
- : current;
38
-
39
- function notify() {
40
- markStateDirty(instance);
41
- version.update((v) => v + 1);
42
- }
43
-
44
- return {
45
- descriptor: {
46
- get() {
47
- version();
48
- return proxy;
49
- },
50
- set(value: unknown) {
51
- current = value;
52
- proxy =
53
- value !== null && typeof value === "object"
54
- ? deepProxy(value, notify)
55
- : value;
56
- notify();
57
- },
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();
58
70
  },
59
- };
60
- },
71
+ },
72
+ init(instance, initialValue) {
73
+ instance[slot] = new DeepStateCell(instance, initialValue);
74
+ },
75
+ };
61
76
  });
62
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
  }