@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,43 +1,44 @@
1
- import type { StatefulComponent } from "@praxisjs/core";
2
1
  import { type HistoryElement, history } from "@praxisjs/core/internal";
3
2
  import type { Signal } from "@praxisjs/shared";
4
3
 
5
- import { createFieldDecorator } from "../create-field-decorator";
4
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
5
+ import { addLifecycleHooks } from "../lifecycle";
6
6
 
7
7
  export type HistoryOf<C, K extends keyof C> = HistoryElement<C[K]>;
8
8
 
9
- export function History(fieldName: string, limit = 50) {
10
- return createFieldDecorator({
11
- bind(instance: StatefulComponent, _name: string) {
12
- const inst = instance as unknown as Record<string, unknown>;
13
- let h: HistoryElement<unknown> | undefined;
14
-
15
- const source = () => inst[fieldName];
9
+ function createHistory(host: FieldHost, fieldName: string, limit: number): HistoryElement<unknown> {
10
+ const h = history((() => host[fieldName]) as Signal<unknown>, limit);
11
+ const undo = h.undo.bind(h);
12
+ const redo = h.redo.bind(h);
13
+ h.undo = () => {
14
+ const prev = h.values()[h.values().length - 2];
15
+ if (prev === undefined) return;
16
+ undo();
17
+ host[fieldName] = prev;
18
+ };
19
+ h.redo = () => {
20
+ redo();
21
+ host[fieldName] = h.current();
22
+ };
23
+ return h;
24
+ }
16
25
 
17
- return {
18
- descriptor: {
19
- get(): HistoryElement<unknown> {
20
- if (!h) {
21
- const hInst = history(source as Signal<unknown>, limit);
22
- const _undo = hInst.undo.bind(hInst);
23
- const _redo = hInst.redo.bind(hInst);
24
- hInst.undo = () => {
25
- const prev = hInst.values()[hInst.values().length - 2];
26
- if (prev === undefined) return;
27
- _undo();
28
- inst[fieldName] = prev;
29
- };
30
- hInst.redo = () => {
31
- _redo();
32
- inst[fieldName] = hInst.current();
33
- };
34
- h = hInst;
35
- }
36
- return h;
37
- },
38
- enumerable: false,
26
+ export function History(fieldName: string, limit = 50): FieldDecorator {
27
+ return sharedField((name) => {
28
+ const slot = Symbol(`praxis.history.${name}`);
29
+ // The tracking effect would otherwise keep recording a source that outlives the component.
30
+ const destroy = (instance: object) => { ((instance as FieldHost)[slot] as HistoryElement<unknown> | undefined)?.destroy(); };
31
+ return {
32
+ descriptor: {
33
+ enumerable: false,
34
+ // Created on first read, so an instance whose history is never read records nothing.
35
+ get(this: FieldHost) {
36
+ return (this[slot] ??= createHistory(this, fieldName, limit));
39
37
  },
40
- };
41
- },
38
+ },
39
+ init(instance) {
40
+ addLifecycleHooks(instance, undefined, destroy);
41
+ },
42
+ };
42
43
  });
43
44
  }
@@ -10,3 +10,4 @@ export { Resource, invalidateResource, type ResourceInstance, type ResourceOptio
10
10
  export { Synced } from "./synced";
11
11
  export { DeepState } from "./deep-state";
12
12
  export { Ref, createRef } from "./ref";
13
+ export { Selector, type SelectorOptions } from "./selector";
@@ -1,43 +1,57 @@
1
- import { persistedSignal, type PersistedSignalOptions } from "@praxisjs/core/internal";
2
- import type { Signal } from "@praxisjs/shared";
1
+ import {
2
+ persistedSignal,
3
+ type PersistedSignal,
4
+ type PersistedSignalOptions,
5
+ } from "@praxisjs/core/internal";
3
6
 
4
- import { createFieldDecorator } from "../create-field-decorator";
7
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
8
+ import { addLifecycleHooks } from "../lifecycle";
5
9
 
6
- const signalMap = new WeakMap<object, Map<string, Signal<unknown>>>();
10
+ // Fields of one instance that persist under the same key share one signal. Only read while the
11
+ // instance is built; each field then reads its signal from its own slot.
12
+ const SIGNALS: unique symbol = Symbol("praxis.persistedSignals");
13
+
14
+ interface PersistedHost {
15
+ [SIGNALS]?: Map<string, PersistedSignal<unknown>>;
16
+ }
7
17
 
8
18
  function getOrCreateSignal<T>(
9
- instance: object,
19
+ instance: PersistedHost,
10
20
  storageKey: string,
11
21
  initialValue: T,
12
22
  options: PersistedSignalOptions<T>,
13
- ): Signal<T> {
14
- if (!signalMap.has(instance)) signalMap.set(instance, new Map());
15
- const map = signalMap.get(instance) as Map<string, Signal<unknown>>;
16
- if (!map.has(storageKey)) {
17
- map.set(
18
- storageKey,
19
- persistedSignal(storageKey, initialValue, options) as Signal<unknown>,
20
- );
23
+ ): PersistedSignal<T> {
24
+ const map = (instance[SIGNALS] ??= new Map<string, PersistedSignal<unknown>>());
25
+ let sig = map.get(storageKey);
26
+ if (!sig) {
27
+ sig = persistedSignal(storageKey, initialValue, options) as PersistedSignal<unknown>;
28
+ map.set(storageKey, sig);
21
29
  }
22
- return map.get(storageKey) as Signal<T>;
30
+ return sig as PersistedSignal<T>;
23
31
  }
24
32
 
25
33
  export function Persisted<T>(
26
34
  key?: string,
27
35
  options: PersistedSignalOptions<T> = {},
28
- ) {
29
- return createFieldDecorator({
30
- bind(instance, name, initialValue) {
31
- const storageKey = key ?? name;
32
- getOrCreateSignal(instance, storageKey, initialValue as T, options);
33
- return {
34
- descriptor: {
35
- get: () =>
36
- getOrCreateSignal(instance, storageKey, undefined as T, options)(),
37
- set: (value: T) =>
38
- { getOrCreateSignal(instance, storageKey, undefined as T, options).set(value); },
36
+ ): FieldDecorator {
37
+ return sharedField((name) => {
38
+ const slot = Symbol(`praxis.persisted.${name}`);
39
+ const storageKey = key ?? name;
40
+ // Without this every mounted instance would leave a window "storage" listener behind.
41
+ const close = (instance: object) => { ((instance as FieldHost)[slot] as PersistedSignal<T>).close(); };
42
+ return {
43
+ descriptor: {
44
+ get(this: FieldHost) {
45
+ return (this[slot] as PersistedSignal<T>)();
46
+ },
47
+ set(this: FieldHost, value: T) {
48
+ (this[slot] as PersistedSignal<T>).set(value);
39
49
  },
40
- };
41
- },
50
+ },
51
+ init(instance, initialValue) {
52
+ instance[slot] = getOrCreateSignal(instance, storageKey, initialValue as T, options);
53
+ addLifecycleHooks(instance, undefined, close);
54
+ },
55
+ };
42
56
  });
43
57
  }
@@ -1,31 +1,39 @@
1
+ import type { StatefulComponent } from "@praxisjs/core";
1
2
  import {
2
3
  getComponentDefault,
3
4
  getComponentRawProp,
4
5
  setComponentDefault,
5
6
  } from "@praxisjs/core/internal";
6
7
 
7
- import { createFieldDecorator } from "../create-field-decorator";
8
+ import { addClaimableInitializer, assertFieldContext } from "../field";
8
9
 
9
10
  export function Prop() {
10
- return createFieldDecorator({
11
- bind(instance, name, initialValue) {
12
- setComponentDefault(instance, name, initialValue);
13
- return {
14
- descriptor: {
15
- get() {
16
- const fromParent = getComponentRawProp(instance, name);
17
- if (fromParent !== undefined) {
18
- return typeof fromParent === "function"
19
- ? (fromParent as () => unknown)()
20
- : fromParent;
21
- }
22
- return getComponentDefault(instance, name);
23
- },
24
- set(value: unknown) {
25
- setComponentDefault(instance, name, value);
26
- },
27
- },
28
- };
29
- },
30
- });
11
+ return function <This extends StatefulComponent, Value = unknown>(
12
+ _value: undefined,
13
+ context: ClassFieldDecoratorContext<This, Value>,
14
+ ): void {
15
+ assertFieldContext(context, "@Prop()");
16
+ const name = context.name as string;
17
+ // Shared by every instance, like @State's accessors.
18
+ const descriptor: PropertyDescriptor = {
19
+ enumerable: true,
20
+ configurable: true,
21
+ get(this: object) {
22
+ const fromParent = getComponentRawProp(this, name);
23
+ if (fromParent !== undefined) {
24
+ return typeof fromParent === "function"
25
+ ? (fromParent as () => unknown)()
26
+ : fromParent;
27
+ }
28
+ return getComponentDefault(this, name);
29
+ },
30
+ set(this: object, value: unknown) {
31
+ setComponentDefault(this, name, value);
32
+ },
33
+ };
34
+ addClaimableInitializer(context, function (value) {
35
+ setComponentDefault(this, name, value);
36
+ Object.defineProperty(this, name, descriptor);
37
+ });
38
+ };
31
39
  }
@@ -1,11 +1,10 @@
1
- import { createFieldDecorator } from "../create-field-decorator";
1
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
2
2
 
3
3
  export type Ref<T extends Element = Element> = ((el: T | null) => void) & {
4
4
  current: T | null;
5
5
  };
6
6
 
7
7
  /** Creates a standalone ref object for use outside of class fields (e.g. module-level or passed to @Lazy). */
8
-
9
8
  export function createRef<T extends Element = Element>(): Ref<T> {
10
9
  function refFn(el: T | null): void {
11
10
  (refFn as Ref<T>).current = el;
@@ -15,13 +14,16 @@ export function createRef<T extends Element = Element>(): Ref<T> {
15
14
  }
16
15
 
17
16
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
18
- export function Ref<T extends Element = Element>() {
19
- return createFieldDecorator({
20
- bind(_instance, _name, _initialValue) {
21
- const ref = createRef<T>();
22
- return {
23
- descriptor: { get: () => ref },
24
- };
25
- },
17
+ export function Ref<T extends Element = Element>(): FieldDecorator {
18
+ return sharedField((name) => {
19
+ const ref = Symbol(`praxis.ref.${name}`);
20
+ return {
21
+ descriptor: {
22
+ // Created on first read: a ref that is never handed to an element costs nothing.
23
+ get(this: FieldHost) {
24
+ return (this[ref] ??= createRef<T>());
25
+ },
26
+ },
27
+ };
26
28
  });
27
29
  }
@@ -4,7 +4,8 @@ import {
4
4
  type ResourceOptions,
5
5
  } from "@praxisjs/core/internal";
6
6
 
7
- import { createFieldDecorator, type FieldBinding } from "../create-field-decorator";
7
+ import { type FieldHost, sharedField } from "../field";
8
+ import { addLifecycleHooks } from "../lifecycle";
8
9
 
9
10
  export { invalidateResource } from "@praxisjs/core/internal";
10
11
  export type { ResourceInstance, ResourceOptions };
@@ -30,22 +31,26 @@ export function Resource<T>(
30
31
  fetcher: (() => Promise<T>) | ((self: any) => Promise<T>),
31
32
  options: ResourceOptions<T> = {},
32
33
  ): FieldDecoratorReturn {
33
- return createFieldDecorator({
34
- bind(instance, _name, _initialValue): FieldBinding {
35
- const bound =
36
- fetcher.length > 0
37
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
- ? () => (fetcher as (self: any) => Promise<T>)(instance)
39
- : (fetcher as () => Promise<T>);
40
- const r = resource(bound, options);
41
- return {
42
- descriptor: {
43
- get(): ResourceInstance<T> { return r; },
44
- // eslint-disable-next-line @typescript-eslint/no-empty-function
45
- set(): void {},
34
+ return sharedField((name) => {
35
+ const slot = Symbol(`praxis.resource.${name}`);
36
+ const destroy = (instance: object) => { ((instance as FieldHost)[slot] as ResourceInstance<T>).destroy(); };
37
+ return {
38
+ descriptor: {
39
+ get(this: FieldHost): ResourceInstance<T> {
40
+ return this[slot] as ResourceInstance<T>;
46
41
  },
47
- onUnmount() { r.destroy(); },
48
- };
49
- },
42
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
43
+ set(): void {},
44
+ },
45
+ init(instance) {
46
+ const bound =
47
+ fetcher.length > 0
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ ? () => (fetcher as (self: any) => Promise<T>)(instance)
50
+ : (fetcher as () => Promise<T>);
51
+ instance[slot] = resource(bound, options);
52
+ addLifecycleHooks(instance, undefined, destroy);
53
+ },
54
+ };
50
55
  }) as unknown as FieldDecoratorReturn;
51
56
  }
@@ -0,0 +1,45 @@
1
+ import { selector, type Selector as SelectorFn, type SelectorEquals } from "@praxisjs/core/internal";
2
+ import type { Computed } from "@praxisjs/shared";
3
+ import { isComputed } from "@praxisjs/shared/internal";
4
+
5
+ import { type FieldHost, sharedField } from "../field";
6
+
7
+ export type Selector<K> = SelectorFn<K>;
8
+
9
+ export interface SelectorOptions<K, T> {
10
+ /**
11
+ * Decides whether `key` is selected for the source value `value`. Defaults to a strict
12
+ * comparison (SameValueZero), which notifies only the previous and next keys; a custom function
13
+ * is evaluated against every key in use whenever the source changes.
14
+ */
15
+ equals?: SelectorEquals<K, T>;
16
+ }
17
+
18
+ function createSelector<K, T>(host: FieldHost, source: string, equals: SelectorEquals<K, T> | undefined): SelectorFn<K> {
19
+ const read = () => {
20
+ const raw = host[source];
21
+ return (isComputed(raw) ? (raw as Computed<unknown>)() : raw) as T;
22
+ };
23
+ return equals ? selector(read, equals) : (selector(read) as unknown as SelectorFn<K>);
24
+ }
25
+
26
+ export function Selector<const Name extends string, K = unknown, T = unknown>(
27
+ source: Name,
28
+ options: SelectorOptions<K, T> = {},
29
+ ) {
30
+ const decorator = sharedField<object>((name) => {
31
+ const slot = Symbol(`praxis.selector.${name}`);
32
+ return {
33
+ descriptor: {
34
+ // Created on first read, so the source field may be declared after this one.
35
+ get(this: FieldHost) {
36
+ return (this[slot] ??= createSelector(this, source, options.equals));
37
+ },
38
+ },
39
+ };
40
+ });
41
+ return decorator as <This extends Record<Name, unknown>, Value extends Selector<never>>(
42
+ value: undefined,
43
+ context: ClassFieldDecoratorContext<This, Value>,
44
+ ) => void;
45
+ }
@@ -1,6 +1,11 @@
1
- import { createFieldDecorator } from "../create-field-decorator";
1
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
2
2
 
3
- const slotsMap = new WeakMap<object, Map<string, unknown[]>>();
3
+ // On the instance rather than in a WeakMap: every slot field read goes through here.
4
+ const SLOTS: unique symbol = Symbol("praxis.slots");
5
+
6
+ interface SlotHost {
7
+ [SLOTS]?: Map<string, unknown[]>;
8
+ }
4
9
 
5
10
  function resolveSlots(children: unknown): Map<string, unknown[]> {
6
11
  const slots = new Map<string, unknown[]>();
@@ -18,8 +23,9 @@ function resolveSlots(children: unknown): Map<string, unknown[]> {
18
23
  const slotName = child.getAttribute("slot");
19
24
  if (slotName) {
20
25
  child.removeAttribute("slot");
21
- if (!slots.has(slotName)) slots.set(slotName, []);
22
- (slots.get(slotName) as unknown[]).push(child);
26
+ let named = slots.get(slotName);
27
+ if (!named) slots.set(slotName, (named = []));
28
+ named.push(child);
23
29
  continue;
24
30
  }
25
31
  }
@@ -30,29 +36,29 @@ function resolveSlots(children: unknown): Map<string, unknown[]> {
30
36
  }
31
37
 
32
38
  export function initSlots(instance: object, children: unknown): void {
33
- slotsMap.set(instance, resolveSlots(children));
39
+ (instance as SlotHost)[SLOTS] = resolveSlots(children);
34
40
  }
35
41
 
36
42
  export function getSlot(instance: object, name: string): unknown[] {
37
- return slotsMap.get(instance)?.get(name) ?? [];
43
+ return (instance as SlotHost)[SLOTS]?.get(name) ?? [];
38
44
  }
39
45
 
40
- export function Slot(name?: string) {
41
- return createFieldDecorator({
42
- bind(instance, propName, _initialValue) {
43
- const slotName = name ?? (propName === "default" ? "default" : propName);
44
- return {
45
- descriptor: {
46
- get: () => getSlot(instance, slotName),
47
- set: (_value: unknown) => {
48
- if (process.env.NODE_ENV !== "production") {
49
- console.warn(
50
- `[Slot] "${propName}" is a slot and cannot be assigned directly. Slots are filled by the parent component.`,
51
- );
52
- }
53
- },
46
+ export function Slot(name?: string): FieldDecorator {
47
+ return sharedField((propName) => {
48
+ const slotName = name ?? propName;
49
+ return {
50
+ descriptor: {
51
+ get(this: FieldHost) {
52
+ return getSlot(this, slotName);
53
+ },
54
+ set(_value: unknown) {
55
+ if (process.env.NODE_ENV !== "production") {
56
+ console.warn(
57
+ `[Slot] "${propName}" is a slot and cannot be assigned directly. Slots are filled by the parent component.`,
58
+ );
59
+ }
54
60
  },
55
- };
56
- },
61
+ },
62
+ };
57
63
  });
58
64
  }
@@ -1,22 +1,38 @@
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 { addClaimableInitializer, assertFieldContext } from "../field";
4
4
 
5
5
  import type { ReactiveHost } from "../reactive-host";
6
6
 
7
+ type StateHost = Record<PropertyKey, unknown>;
8
+
7
9
  export function State() {
8
- return createFieldDecorator<ReactiveHost>({
9
- bind(instance, _name, initialValue) {
10
- const sig = signal(initialValue);
11
- return {
12
- descriptor: {
13
- get: () => sig(),
14
- set: (value: unknown) => {
15
- markStateDirty(instance);
16
- sig.set(value);
17
- },
18
- },
19
- };
20
- },
21
- });
10
+ return function <This extends ReactiveHost, Value = unknown>(
11
+ _value: undefined,
12
+ context: ClassFieldDecoratorContext<This, Value>,
13
+ ): void {
14
+ assertFieldContext(context, "@State()");
15
+ const name = context.name;
16
+ // One accessor pair per decorated field, shared by every instance: per-instance closures would
17
+ // give each instance its own hidden class, turning every field read into a megamorphic access.
18
+ // It stays an own property of each instance, so decorators stacked above @State (and custom ones
19
+ // built with createFieldDecorator) find and wrap it, and a Proxy around the instance works.
20
+ const node = Symbol(`praxis.state.${String(name)}`);
21
+ const descriptor: PropertyDescriptor = {
22
+ enumerable: true,
23
+ configurable: true,
24
+ get(this: StateHost) {
25
+ return readSignal(this[node] as SignalNode);
26
+ },
27
+ set(this: StateHost, value: unknown) {
28
+ markStateDirty(this);
29
+ writeSignal(this[node] as SignalNode, value);
30
+ },
31
+ };
32
+ addClaimableInitializer(context, function (value) {
33
+ const instance = this as unknown as StateHost;
34
+ instance[node] = new SignalNode(value);
35
+ Object.defineProperty(instance, name, descriptor);
36
+ });
37
+ };
22
38
  }
@@ -1,28 +1,40 @@
1
1
  import { syncedSignal, type SyncedSignal } from "@praxisjs/core/internal";
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
- const signalMap = new WeakMap<object, Map<string, SyncedSignal<unknown>>>();
6
+ // Fields of one instance synced over the same channel share one signal. Only read while the
7
+ // instance is built; each field then reads its signal from its own slot.
8
+ const SIGNALS: unique symbol = Symbol("praxis.syncedSignals");
6
9
 
7
- export function Synced(channelName?: string) {
8
- return createFieldDecorator({
9
- bind(instance, name, initialValue) {
10
- const key = channelName ?? name;
11
-
12
- if (!signalMap.has(instance)) signalMap.set(instance, new Map());
13
- const map = signalMap.get(instance) as Map<string, SyncedSignal<unknown>>;
14
- if (!map.has(key)) map.set(key, syncedSignal(key, initialValue));
15
- const sig = map.get(key) as SyncedSignal<unknown>;
10
+ interface SyncedHost {
11
+ [SIGNALS]?: Map<string, SyncedSignal<unknown>>;
12
+ }
16
13
 
17
- return {
18
- descriptor: {
19
- get: () => sig(),
20
- set: (value: unknown) => { sig.set(value); },
14
+ export function Synced(channelName?: string): FieldDecorator {
15
+ return sharedField((name) => {
16
+ const slot = Symbol(`praxis.synced.${name}`);
17
+ const key = channelName ?? name;
18
+ const close = (instance: object) => { ((instance as FieldHost)[slot] as SyncedSignal<unknown>).close(); };
19
+ return {
20
+ descriptor: {
21
+ get(this: FieldHost) {
22
+ return (this[slot] as SyncedSignal<unknown>)();
21
23
  },
22
- onUnmount() {
23
- sig.close();
24
+ set(this: FieldHost, value: unknown) {
25
+ (this[slot] as SyncedSignal<unknown>).set(value);
24
26
  },
25
- };
26
- },
27
+ },
28
+ init(instance, initialValue) {
29
+ const map = ((instance as SyncedHost)[SIGNALS] ??= new Map<string, SyncedSignal<unknown>>());
30
+ let sig = map.get(key);
31
+ if (!sig) {
32
+ sig = syncedSignal(key, initialValue);
33
+ map.set(key, sig);
34
+ }
35
+ instance[slot] = sig;
36
+ addLifecycleHooks(instance, undefined, close);
37
+ },
38
+ };
27
39
  });
28
40
  }