@praxisjs/decorators 1.1.1 → 1.3.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.
@@ -4,6 +4,8 @@ import { StatefulComponent } from "@praxisjs/core";
4
4
  import { signal, computed } from "@praxisjs/core/internal";
5
5
 
6
6
  import { createFieldDecorator } from "../create-field-decorator";
7
+ import { createGetterDecorator } from "../create-getter-decorator";
8
+ import { createAccessorDecorator } from "../create-accessor-decorator";
7
9
  import { Watch } from "../functions/watch";
8
10
  import { When } from "../functions/when";
9
11
  import { Compose } from "../properties/compose";
@@ -152,39 +154,243 @@ describe("Prop decorator", () => {
152
154
 
153
155
  describe("Computed decorator", () => {
154
156
  it("wraps a getter in a computed — result is memoized", () => {
155
- const fn = vi.fn(function (this: { base: number }) {
156
- return this.base * 2;
157
- });
157
+ const s = signal(5);
158
+ const fn = vi.fn(function (this: object) { return s() * 2; });
158
159
 
159
- const wrappedGetter = Computed()(fn, {} as ClassGetterDecoratorContext);
160
- const instance = { base: 5 } as unknown as StatefulComponent;
160
+ const { ctx, run } = getterCtx("doubled");
161
+ Computed()(fn, ctx);
162
+ const instance = {} as unknown as StatefulComponent;
163
+ run(instance);
161
164
 
162
- const result1 = wrappedGetter.call(instance);
163
- const result2 = wrappedGetter.call(instance);
165
+ const result1 = (instance as unknown as Record<string, unknown>).doubled;
166
+ const result2 = (instance as unknown as Record<string, unknown>).doubled;
164
167
  expect(result1).toBe(10);
165
168
  expect(result2).toBe(10);
166
- expect(fn).toHaveBeenCalledTimes(1); // lazy caching
169
+ expect(fn).toHaveBeenCalledTimes(1);
167
170
  });
168
171
 
169
172
  it("recomputes when underlying signal changes", () => {
170
173
  const s = signal(3);
171
174
  const getter = function (this: object) { return s() * 2; };
172
- const wrapped = Computed()(getter, {} as ClassGetterDecoratorContext);
175
+ const { ctx, run } = getterCtx("val");
176
+ Computed()(getter, ctx);
173
177
  const obj = {} as unknown as StatefulComponent;
174
- expect(wrapped.call(obj)).toBe(6);
178
+ run(obj);
179
+
180
+ expect((obj as unknown as Record<string, unknown>).val).toBe(6);
175
181
  s.set(5);
176
- expect(wrapped.call(obj)).toBe(10);
182
+ expect((obj as unknown as Record<string, unknown>).val).toBe(10);
177
183
  });
178
184
 
179
185
  it("is per-instance — separate computed per object", () => {
180
186
  const s = signal(1);
181
187
  const getter = function (this: object) { return s(); };
182
- const wrapped = Computed()(getter, {} as ClassGetterDecoratorContext);
188
+ const { ctx, run } = getterCtx("val");
189
+ Computed()(getter, ctx);
183
190
  const a = {} as unknown as StatefulComponent;
184
191
  const b = {} as unknown as StatefulComponent;
185
- expect(wrapped.call(a)).toBe(1);
186
- expect(wrapped.call(b)).toBe(1);
187
- // Both share the same signal but have independent computed instances
192
+ run(a);
193
+ run(b);
194
+
195
+ expect((a as unknown as Record<string, unknown>).val).toBe(1);
196
+ expect((b as unknown as Record<string, unknown>).val).toBe(1);
197
+ });
198
+ });
199
+
200
+ // ── Computed (writable) ───────────────────────────────────────────────────────
201
+
202
+ function getterCtx(name: string) {
203
+ const initializers: Array<(this: unknown) => void> = [];
204
+ return {
205
+ ctx: {
206
+ name,
207
+ kind: "getter" as const,
208
+ addInitializer(fn: (this: unknown) => void) {
209
+ initializers.push(fn);
210
+ },
211
+ } as unknown as ClassGetterDecoratorContext,
212
+ run(instance: unknown) {
213
+ initializers.forEach((fn) => { fn.call(instance); });
214
+ },
215
+ };
216
+ }
217
+
218
+ describe("Computed decorator — writable (set option)", () => {
219
+ it("setter is called when property is assigned", () => {
220
+ const written: number[] = [];
221
+ const { ctx, run } = getterCtx("double");
222
+
223
+ const s = signal(3);
224
+ const getter = function (this: object) { return s() * 2; };
225
+ Computed({ set: (v: unknown) => { written.push(v as number); } })(getter, ctx);
226
+
227
+ const instance = {} as unknown as StatefulComponent;
228
+ run(instance);
229
+
230
+ (instance as unknown as Record<string, unknown>).double = 42;
231
+ expect(written).toEqual([42]);
232
+ });
233
+
234
+ it("setter receives the component instance as this", () => {
235
+ let capturedThis: unknown;
236
+ const { ctx, run } = getterCtx("val");
237
+
238
+ const getter = function (this: object) { return 0; };
239
+ Computed({ set(this: unknown, _v: unknown) { capturedThis = this; } })(getter, ctx);
240
+
241
+ const instance = { id: "target" } as unknown as StatefulComponent;
242
+ run(instance);
243
+
244
+ (instance as unknown as Record<string, unknown>).val = 99;
245
+ expect(capturedThis).toBe(instance);
246
+ });
247
+
248
+ it("getter still returns the computed value after set option is added", () => {
249
+ const s = signal(5);
250
+ const { ctx, run } = getterCtx("doubled");
251
+
252
+ const getter = function (this: object) { return s() * 2; };
253
+ Computed({ set: (_v: unknown) => { /* no-op */ } })(getter, ctx);
254
+
255
+ const instance = {} as unknown as StatefulComponent;
256
+ run(instance);
257
+
258
+ expect((instance as unknown as Record<string, unknown>).doubled).toBe(10);
259
+ s.set(7);
260
+ expect((instance as unknown as Record<string, unknown>).doubled).toBe(14);
261
+ });
262
+
263
+ it("setter writes through to underlying signals, updating the getter", () => {
264
+ const s = signal(0);
265
+ const { ctx, run } = getterCtx("celsius");
266
+
267
+ const getter = function (this: object) { return s(); };
268
+ Computed({ set: (v: unknown) => { s.set(v as number); } })(getter, ctx);
269
+
270
+ const instance = {} as unknown as StatefulComponent;
271
+ run(instance);
272
+
273
+ (instance as unknown as Record<string, unknown>).celsius = 100;
274
+ expect((instance as unknown as Record<string, unknown>).celsius).toBe(100);
275
+ });
276
+ });
277
+
278
+ // ── Computed (accessor form) ──────────────────────────────────────────────────
279
+
280
+ describe("Computed decorator — accessor form ({ get, set })", () => {
281
+ function applyAccessorDecorator<T>(
282
+ options: { get: (this: object) => T; set: (this: object, value: T) => void },
283
+ instance: object,
284
+ ): { get: () => T; set: (v: T) => void } {
285
+ const target = {
286
+ get(this: object) { return undefined as T; },
287
+ set(_v: T) { /* original */ },
288
+ };
289
+ const ctx = { kind: "accessor", name: "value" } as unknown as ClassAccessorDecoratorContext<object, T>;
290
+ const result = Computed(options)(target, ctx);
291
+ result.init!.call(instance, undefined as unknown as T);
292
+ return {
293
+ get: () => result.get!.call(instance),
294
+ set: (v: T) => result.set!.call(instance, v),
295
+ };
296
+ }
297
+
298
+ it("get function is wrapped in a computed — result is memoized", () => {
299
+ const s = signal(3);
300
+ const getFn = vi.fn(function (this: object) { return s() * 2; });
301
+ const instance = {};
302
+ const { get } = applyAccessorDecorator({ get: getFn, set: () => { /* no-op */ } }, instance);
303
+
304
+ expect(get()).toBe(6);
305
+ expect(get()).toBe(6);
306
+ expect(getFn).toHaveBeenCalledTimes(1);
307
+ });
308
+
309
+ it("recomputes when underlying signal changes", () => {
310
+ const s = signal(10);
311
+ const instance = {};
312
+ const { get } = applyAccessorDecorator({
313
+ get(this: object) { return s() + 1; },
314
+ set() { /* no-op */ },
315
+ }, instance);
316
+
317
+ expect(get()).toBe(11);
318
+ s.set(20);
319
+ expect(get()).toBe(21);
320
+ });
321
+
322
+ it("set function is called when the setter is invoked", () => {
323
+ const written: number[] = [];
324
+ const instance = {};
325
+ const { set } = applyAccessorDecorator({
326
+ get(this: object) { return 0; },
327
+ set(_v: number) { written.push(_v); },
328
+ }, instance);
329
+
330
+ set(42);
331
+ expect(written).toEqual([42]);
332
+ });
333
+
334
+ it("set function receives the component instance as this", () => {
335
+ let capturedThis: unknown;
336
+ const instance = { id: "target" };
337
+ const { set } = applyAccessorDecorator({
338
+ get(this: object) { return 0; },
339
+ set(this: unknown) { capturedThis = this; },
340
+ }, instance);
341
+
342
+ set(1);
343
+ expect(capturedThis).toBe(instance);
344
+ });
345
+
346
+ it("set writes through to underlying signals, updating the getter", () => {
347
+ const s = signal(0);
348
+ const instance = {};
349
+ const { get, set } = applyAccessorDecorator({
350
+ get(this: object) { return s(); },
351
+ set(this: object, v: number) { s.set(v); },
352
+ }, instance);
353
+
354
+ set(100);
355
+ expect(get()).toBe(100);
356
+ });
357
+
358
+ it("is per-instance — separate computed per object", () => {
359
+ const s = signal(5);
360
+ const options = {
361
+ get(this: object) { return s() * 2; },
362
+ set() { /* no-op */ },
363
+ };
364
+ const a = {};
365
+ const b = {};
366
+ const decA = applyAccessorDecorator(options, a);
367
+ const decB = applyAccessorDecorator(options, b);
368
+
369
+ expect(decA.get()).toBe(10);
370
+ expect(decB.get()).toBe(10);
371
+ s.set(7);
372
+ expect(decA.get()).toBe(14);
373
+ expect(decB.get()).toBe(14);
374
+ });
375
+
376
+ it("accessor form without set — setter is a no-op", () => {
377
+ const s = signal(5);
378
+ const instance = {};
379
+ const target = { get() { return undefined as unknown as number; }, set() { /* original */ } };
380
+ const ctx = { kind: "accessor", name: "value" } as unknown as ClassAccessorDecoratorContext<object, number>;
381
+ // Bypass overload constraint — optionsSet is undefined at runtime
382
+ type AccessorDecoFactory = (opts: { get: (this: object) => number }) => (
383
+ t: typeof target,
384
+ c: typeof ctx,
385
+ ) => ClassAccessorDecoratorResult<object, number>;
386
+ const result = (Computed as unknown as AccessorDecoFactory)(
387
+ { get(this: object) { return s() * 2; } },
388
+ )(target, ctx);
389
+ result.init!.call(instance, 0);
390
+
391
+ expect(result.get!.call(instance)).toBe(10);
392
+ expect(() => { result.set!.call(instance, 99); }).not.toThrow();
393
+ expect(result.get!.call(instance)).toBe(10); // unchanged — no setter
188
394
  });
189
395
  });
190
396
 
@@ -549,6 +755,78 @@ describe("When decorator", () => {
549
755
  await Promise.resolve();
550
756
  expect(handler).toHaveBeenCalledTimes(1);
551
757
  });
758
+
759
+ it("fires immediately when the watched prop is a plain truthy value (non-signal)", () => {
760
+ const { ctx, run } = methodCtxFor("onPlain");
761
+ const handler = vi.fn();
762
+ When("plainProp")(handler, ctx as unknown as ClassMethodDecoratorContext);
763
+
764
+ const instance = new TestComponent();
765
+ (instance as unknown as Record<string, unknown>).plainProp = "hello";
766
+ run(instance);
767
+ (instance as unknown as { onMount: () => void }).onMount?.();
768
+
769
+ expect(handler).toHaveBeenCalledTimes(1);
770
+ expect(handler).toHaveBeenCalledWith("hello");
771
+ });
772
+
773
+ it("condition: fires when condition returns true", async () => {
774
+ const { ctx, run } = methodCtxFor("onHigh");
775
+ const handler = vi.fn();
776
+ When("count", (v: number) => v > 5)(handler, ctx as unknown as ClassMethodDecoratorContext);
777
+
778
+ const s = signal(0);
779
+ const instance = new TestComponent();
780
+ (instance as unknown as Record<string, unknown>).count = s;
781
+ run(instance);
782
+ (instance as unknown as { onMount: () => void }).onMount?.();
783
+
784
+ s.set(3); // condition false — should not fire
785
+ await Promise.resolve();
786
+ expect(handler).not.toHaveBeenCalled();
787
+
788
+ s.set(7); // condition true — should fire
789
+ await Promise.resolve();
790
+ expect(handler).toHaveBeenCalledTimes(1);
791
+ expect(handler).toHaveBeenCalledWith(7);
792
+ });
793
+
794
+ it("condition: does not fire when value is truthy but condition returns false", async () => {
795
+ const { ctx, run } = methodCtxFor("onPositive");
796
+ const handler = vi.fn();
797
+ When("n", (v: number) => v > 100)(handler, ctx as unknown as ClassMethodDecoratorContext);
798
+
799
+ const s = signal(0);
800
+ const instance = new TestComponent();
801
+ (instance as unknown as Record<string, unknown>).n = s;
802
+ run(instance);
803
+ (instance as unknown as { onMount: () => void }).onMount?.();
804
+
805
+ s.set(1); // truthy but condition fails
806
+ s.set(50); // truthy but condition fails
807
+ await Promise.resolve();
808
+ expect(handler).not.toHaveBeenCalled();
809
+ });
810
+
811
+ it("condition: fires on the first value satisfying the condition, then stops", async () => {
812
+ const { ctx, run } = methodCtxFor("onFirst");
813
+ const handler = vi.fn();
814
+ When("v", (v: number) => v >= 10)(handler, ctx as unknown as ClassMethodDecoratorContext);
815
+
816
+ const s = signal(0);
817
+ const instance = new TestComponent();
818
+ (instance as unknown as Record<string, unknown>).v = s;
819
+ run(instance);
820
+ (instance as unknown as { onMount: () => void }).onMount?.();
821
+
822
+ s.set(10); // first match — fires
823
+ await Promise.resolve();
824
+ expect(handler).toHaveBeenCalledTimes(1);
825
+
826
+ s.set(20); // when() already disposed — should not fire again
827
+ await Promise.resolve();
828
+ expect(handler).toHaveBeenCalledTimes(1);
829
+ });
552
830
  });
553
831
 
554
832
  // ── Prop (additional branches) ────────────────────────────────────────────────
@@ -1085,3 +1363,83 @@ describe("createFieldDecorator — additional property", () => {
1085
1363
  expect((instance as unknown as Record<string, unknown>).extraProp).toBe("extra");
1086
1364
  });
1087
1365
  });
1366
+
1367
+ // ── createGetterDecorator ─────────────────────────────────────────────────────
1368
+
1369
+ describe("createGetterDecorator", () => {
1370
+ it("calls behavior.wrap on access and returns the wrapped value", () => {
1371
+ const wrap = vi.fn((original: (this: object) => unknown, instance: object) => {
1372
+ return () => (original.call(instance) as number) * 2;
1373
+ });
1374
+ const decorator = createGetterDecorator({ wrap });
1375
+ const originalGetter = function (this: object) { return 5 as unknown; };
1376
+ const ctx = { kind: "getter" } as ClassGetterDecoratorContext<object, number>;
1377
+ const wrapped = decorator(originalGetter as (this: object) => number, ctx);
1378
+
1379
+ const instance = {};
1380
+ expect(wrapped.call(instance)).toBe(10);
1381
+ expect(wrap).toHaveBeenCalledWith(originalGetter, instance);
1382
+ });
1383
+
1384
+ it("passes the correct instance as this to behavior.wrap", () => {
1385
+ let capturedInstance: unknown;
1386
+ const decorator = createGetterDecorator({
1387
+ wrap(_original, instance) {
1388
+ capturedInstance = instance;
1389
+ return () => 0;
1390
+ },
1391
+ });
1392
+ const getter = function (this: object) { return 0 as unknown; };
1393
+ const ctx = { kind: "getter" } as ClassGetterDecoratorContext<object, number>;
1394
+ const wrapped = decorator(getter as (this: object) => number, ctx);
1395
+
1396
+ const instance = { id: "test-instance" };
1397
+ wrapped.call(instance);
1398
+ expect(capturedInstance).toBe(instance);
1399
+ });
1400
+ });
1401
+
1402
+ // ── createAccessorDecorator — init ────────────────────────────────────────────
1403
+
1404
+ describe("createAccessorDecorator — init", () => {
1405
+ it("init stores the initial value that bind receives on first get", () => {
1406
+ let capturedInitialValue: unknown = "NOT_SET";
1407
+ const behavior = {
1408
+ bind(_inst: object, _name: string | symbol, initialValue: unknown) {
1409
+ capturedInitialValue = initialValue;
1410
+ return { get: () => initialValue };
1411
+ },
1412
+ };
1413
+ const decorator = createAccessorDecorator(behavior);
1414
+ const target = {
1415
+ get() { return 0; },
1416
+ set() { /* original */ },
1417
+ } as ClassAccessorDecoratorTarget<object, number>;
1418
+ const ctx = { kind: "accessor", name: "val" } as unknown as ClassAccessorDecoratorContext<object, number>;
1419
+ const result = decorator(target, ctx);
1420
+
1421
+ const instance = {};
1422
+ const returned = result.init!.call(instance, 42);
1423
+ result.get!.call(instance); // triggers bind
1424
+ expect(returned).toBe(42);
1425
+ expect(capturedInitialValue).toBe(42);
1426
+ });
1427
+
1428
+ it("init returns the initial value unchanged", () => {
1429
+ const behavior = {
1430
+ bind(_inst: object, _name: string | symbol, initialValue: unknown) {
1431
+ return { get: () => initialValue };
1432
+ },
1433
+ };
1434
+ const decorator = createAccessorDecorator(behavior);
1435
+ const target = {
1436
+ get() { return ""; },
1437
+ set() { /* original */ },
1438
+ } as ClassAccessorDecoratorTarget<object, string>;
1439
+ const ctx = { kind: "accessor", name: "s" } as unknown as ClassAccessorDecoratorContext<object, string>;
1440
+ const result = decorator(target, ctx);
1441
+
1442
+ const instance = {};
1443
+ expect(result.init!.call(instance, "hello")).toBe("hello");
1444
+ });
1445
+ });
@@ -0,0 +1,45 @@
1
+ export interface AccessorBinding {
2
+ get: () => unknown;
3
+ set?: (value: unknown) => void;
4
+ }
5
+
6
+ export interface AccessorBehavior {
7
+ /**
8
+ * Called once per instance on first access. `initialValue` is the value
9
+ * provided by the `accessor` field initializer (e.g. `accessor count = 0`).
10
+ * Returns the get/set pair used by the accessor.
11
+ */
12
+ bind(instance: object, name: string | symbol, initialValue: unknown): AccessorBinding;
13
+ }
14
+
15
+ export function createAccessorDecorator(behavior: AccessorBehavior) {
16
+ return function <This extends object, V>(
17
+ _target: ClassAccessorDecoratorTarget<This, V>,
18
+ context: ClassAccessorDecoratorContext<This, V>,
19
+ ): ClassAccessorDecoratorResult<This, V> {
20
+ const bindings = new WeakMap<object, AccessorBinding>();
21
+ const initValues = new WeakMap<object, unknown>();
22
+
23
+ function getBinding(instance: object): AccessorBinding {
24
+ let b = bindings.get(instance);
25
+ if (!b) {
26
+ b = behavior.bind(instance, context.name, initValues.get(instance));
27
+ bindings.set(instance, b);
28
+ }
29
+ return b;
30
+ }
31
+
32
+ return {
33
+ init(this: This, value: V): V {
34
+ initValues.set(this as object, value);
35
+ return value;
36
+ },
37
+ get(this: This): V {
38
+ return getBinding(this as object).get() as V;
39
+ },
40
+ set(this: This, value: V): void {
41
+ getBinding(this as object).set?.(value as unknown);
42
+ },
43
+ };
44
+ };
45
+ }
@@ -23,6 +23,45 @@ export function createGetterDecorator(behavior: GetterBehavior) {
23
23
  };
24
24
  }
25
25
 
26
+ export interface WritableGetterBinding {
27
+ get: () => unknown;
28
+ set?: (value: unknown) => void;
29
+ }
30
+
31
+ export interface WritableGetterBehavior {
32
+ /**
33
+ * Called once per instance during construction. Returns the get/set pair
34
+ * that will be installed on the instance via `Object.defineProperty`.
35
+ * If `set` is omitted, the property is read-only at runtime (though TypeScript
36
+ * may still see a setter if one is declared in the class body).
37
+ */
38
+ bind(
39
+ instance: object,
40
+ name: string | symbol,
41
+ original: (this: object) => unknown,
42
+ ): WritableGetterBinding;
43
+ }
44
+
45
+ export function createWritableGetterDecorator(behavior: WritableGetterBehavior) {
46
+ return function <This, T>(
47
+ target: (this: This) => T,
48
+ context: ClassGetterDecoratorContext<This, T>,
49
+ ): (this: This) => T {
50
+ context.addInitializer(function (this: unknown) {
51
+ const instance = this as object;
52
+ const binding = behavior.bind(instance, context.name, target as (this: object) => unknown);
53
+ const descriptor: PropertyDescriptor = {
54
+ get: binding.get,
55
+ configurable: true,
56
+ enumerable: false,
57
+ };
58
+ if (binding.set !== undefined) descriptor.set = binding.set;
59
+ Object.defineProperty(instance, context.name, descriptor);
60
+ });
61
+ return target;
62
+ };
63
+ }
64
+
26
65
  export interface GetterObserverBehavior {
27
66
  /**
28
67
  * Called once per instance when the class is initialized.
@@ -1,17 +1,41 @@
1
- import { when } from "@praxisjs/core/internal";
1
+ import { effect } from "@praxisjs/core/internal";
2
2
  import type { Computed, Signal } from "@praxisjs/shared";
3
- import { isComputed } from "@praxisjs/shared/internal";
3
+ import { isComputed, isSignal } from "@praxisjs/shared/internal";
4
4
 
5
5
  import { createLifecycleMethodDecorator } from "../create-lifecycle-method-decorator";
6
6
 
7
- export function When(propName: string) {
7
+ /** Read the current value of a property, unwrapping Signal or Computed if needed. */
8
+ function readProp(instance: Record<string, unknown>, key: string): unknown {
9
+ const raw = instance[key];
10
+ if (isComputed(raw)) return (raw as Computed<unknown>)();
11
+ if (isSignal(raw)) return (raw as Signal<unknown>)();
12
+ return raw;
13
+ }
14
+
15
+ export function When<T = unknown>(
16
+ propName: string,
17
+ condition?: (value: NonNullable<T>) => boolean,
18
+ ) {
8
19
  return createLifecycleMethodDecorator({
9
20
  register(callback, instance) {
10
- const raw = (instance as unknown as Record<string, unknown>)[propName];
11
- const source = isComputed(raw)
12
- ? (raw as Computed<unknown>)
13
- : (raw as Signal<unknown>);
14
- return when(source, (val) => { callback(val); });
21
+ const inst = instance as unknown as Record<string, unknown>;
22
+ let fired = false;
23
+
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;
29
+
30
+ const meetsCondition = condition
31
+ ? val != null && condition(val)
32
+ : val != null && Boolean(val);
33
+
34
+ if (meetsCondition) {
35
+ fired = true;
36
+ callback(val as NonNullable<T>);
37
+ }
38
+ });
15
39
  },
16
40
  });
17
41
  }
package/src/index.ts CHANGED
@@ -54,6 +54,14 @@ export {
54
54
  export {
55
55
  createGetterDecorator,
56
56
  type GetterBehavior,
57
+ createWritableGetterDecorator,
58
+ type WritableGetterBehavior,
59
+ type WritableGetterBinding,
57
60
  createGetterObserverDecorator,
58
61
  type GetterObserverBehavior,
59
62
  } from "./create-getter-decorator";
63
+ export {
64
+ createAccessorDecorator,
65
+ type AccessorBehavior,
66
+ type AccessorBinding,
67
+ } from "./create-accessor-decorator";
@@ -1,18 +1,55 @@
1
1
  import { computed } from "@praxisjs/core/internal";
2
2
 
3
- import { createGetterDecorator } from "../create-getter-decorator";
3
+ import { createAccessorDecorator } from "../create-accessor-decorator";
4
+ import { createWritableGetterDecorator } from "../create-getter-decorator";
4
5
 
5
- export function Computed() {
6
- const cache = new WeakMap<object, () => unknown>();
6
+ export interface ComputedOptions<T = unknown> {
7
+ get?: (this: object) => T;
8
+ set?: (this: object, value: T) => void;
9
+ }
10
+
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>;
21
+
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
+ }
7
45
 
8
- return createGetterDecorator({
9
- wrap(original, instance) {
10
- let c = cache.get(instance);
11
- if (!c) {
12
- c = computed(() => original.call(instance));
13
- cache.set(instance, c);
14
- }
15
- return c;
46
+ return createWritableGetterDecorator({
47
+ bind(instance, _name, original) {
48
+ const c = computed(() => original.call(instance));
49
+ return {
50
+ get: c,
51
+ set: optionsSet ? (value) => { optionsSet.call(instance, value); } : undefined,
52
+ };
16
53
  },
17
54
  });
18
55
  }