trakked 1.2.0 → 2.2.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.
- package/README.md +87 -11
- package/dist/dev/index.cjs +22 -15
- package/dist/dev/index.cjs.map +1 -1
- package/dist/dev/index.d.cts +7 -6
- package/dist/dev/index.d.ts +7 -6
- package/dist/dev/index.js +21 -14
- package/dist/dev/index.js.map +1 -1
- package/dist/prod/index.cjs +22 -15
- package/dist/prod/index.cjs.map +1 -1
- package/dist/prod/index.js +21 -14
- package/dist/prod/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,6 +120,34 @@ To disable coalescing for a specific `string` or `number` property while leaving
|
|
|
120
120
|
accessor version: number = 0; // every increment is its own undo step
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
### Dependency tracking
|
|
124
|
+
|
|
125
|
+
Validators can read other properties of the same model — for example, a `scheduleDays` field might be required only when `isEnabled` is `true`. Trakked automatically tracks which properties each validator reads, and re-runs only the affected validators when those properties change.
|
|
126
|
+
|
|
127
|
+
This works via a lightweight dependency tracking mechanism built into the `@Tracked` getter. Every time a validator runs, Trakked collects every `@Tracked` property that is read during the call. These are recorded as dependencies. When any of those properties is written next, only the validators that declared a dependency on it are re-evaluated — not the entire model.
|
|
128
|
+
|
|
129
|
+
**Consequence for `get`/`set` pairs:** The dependency is registered through the getter, not the setter. If a property is written via a plain setter and its getter is not decorated with `@Tracked`, any validator that reads it will not discover the dependency — and will not re-run when the property changes.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// WRONG — isEnabled getter is plain; validators that read self.isEnabled
|
|
133
|
+
// will not re-run when isEnabled changes
|
|
134
|
+
get isEnabled(): boolean { return this._isEnabled; }
|
|
135
|
+
|
|
136
|
+
@Tracked()
|
|
137
|
+
set isEnabled(value: boolean) { this._isEnabled = value; }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// CORRECT — both getter and setter are decorated
|
|
142
|
+
@Tracked()
|
|
143
|
+
get isEnabled(): boolean { return this._isEnabled; }
|
|
144
|
+
|
|
145
|
+
@Tracked()
|
|
146
|
+
set isEnabled(value: boolean) { this._isEnabled = value; }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
When using `accessor` fields this is never an issue — the getter and setter share the same decoration.
|
|
150
|
+
|
|
123
151
|
### Construction via tracker.construct()
|
|
124
152
|
|
|
125
153
|
All tracked model objects must be created inside `tracker.construct()`. This call:
|
|
@@ -400,7 +428,7 @@ const invoice = tracker.construct(() => new InvoiceModel(tracker));
|
|
|
400
428
|
| `state` | `ObjectState` | Computed DB operation required at save time |
|
|
401
429
|
| `_committedState` | `ObjectState` | The persisted state. Defaults to `Unchanged`. Pass `initialState` to the constructor to override |
|
|
402
430
|
| `destroy()` | `void` | Removes this model from the tracker |
|
|
403
|
-
| `onCommitted()` | `void` | Called automatically by `tracker.onCommit()` —
|
|
431
|
+
| `onCommitted()` | `void` | Called automatically by `tracker.onCommit()` — transitions `_committedState` and records the inverse in the undo stack so that undoing a commit restores the correct state |
|
|
404
432
|
|
|
405
433
|
---
|
|
406
434
|
|
|
@@ -421,6 +449,16 @@ import { ObjectState } from 'trakked';
|
|
|
421
449
|
|
|
422
450
|
`Edited` is **derived**: when `_committedState === Unchanged` and the object has unsaved property changes (`isDirty === true`), `state` returns `Edited`. It is never stored directly.
|
|
423
451
|
|
|
452
|
+
**Undo of a committed save:**
|
|
453
|
+
|
|
454
|
+
`tracker.onCommit()` records the state transition in the undo stack. Undoing past a commit reverses the server operation:
|
|
455
|
+
|
|
456
|
+
| Committed operation | State after undo | Required server operation |
|
|
457
|
+
|---|---|---|
|
|
458
|
+
| INSERT (`New`) | `Deleted` | DELETE |
|
|
459
|
+
| UPDATE (`Edited`) | `Edited` (with pre-edit values) | UPDATE |
|
|
460
|
+
| DELETE (`Deleted`) | `New` | INSERT |
|
|
461
|
+
|
|
424
462
|
**Loading from DB:**
|
|
425
463
|
|
|
426
464
|
Objects default to `Unchanged`, so no extra setup is needed. Property values set inside the constructor are suppressed by `tracker.construct()`:
|
|
@@ -467,12 +505,12 @@ Use this instead of `TrackedObject` when your database is **versioned (temporal)
|
|
|
467
505
|
import {
|
|
468
506
|
VersionedTrackedObject,
|
|
469
507
|
VersionedObjectState,
|
|
470
|
-
|
|
508
|
+
AutoId,
|
|
471
509
|
Tracked,
|
|
472
510
|
} from 'trakked';
|
|
473
511
|
|
|
474
512
|
class OrderModel extends VersionedTrackedObject {
|
|
475
|
-
@
|
|
513
|
+
@AutoId
|
|
476
514
|
id: number = 0;
|
|
477
515
|
|
|
478
516
|
@Tracked()
|
|
@@ -522,7 +560,7 @@ Objects default to `Unchanged`. Set properties inside the constructor — they a
|
|
|
522
560
|
|
|
523
561
|
```typescript
|
|
524
562
|
class OrderModel extends VersionedTrackedObject {
|
|
525
|
-
@
|
|
563
|
+
@AutoId id: number = 0;
|
|
526
564
|
@Tracked() accessor description: string = '';
|
|
527
565
|
constructor(tracker: Tracker, data?: { id: number; description: string }) {
|
|
528
566
|
super(tracker); // initialState defaults to Unchanged
|
|
@@ -664,14 +702,14 @@ import {
|
|
|
664
702
|
VersionedTrackedObject,
|
|
665
703
|
VersionedObjectState,
|
|
666
704
|
Tracked,
|
|
667
|
-
|
|
705
|
+
AutoId,
|
|
668
706
|
TrackedCollection,
|
|
669
707
|
} from 'trakked';
|
|
670
708
|
|
|
671
709
|
const tracker = new Tracker();
|
|
672
710
|
|
|
673
711
|
class OrderLine extends VersionedTrackedObject {
|
|
674
|
-
@
|
|
712
|
+
@AutoId
|
|
675
713
|
id: number = 0;
|
|
676
714
|
|
|
677
715
|
@Tracked((_, v) => !v ? 'Description is required' : undefined)
|
|
@@ -686,7 +724,7 @@ class OrderLine extends VersionedTrackedObject {
|
|
|
686
724
|
}
|
|
687
725
|
|
|
688
726
|
class OrderModel extends VersionedTrackedObject {
|
|
689
|
-
@
|
|
727
|
+
@AutoId
|
|
690
728
|
id: number = 0;
|
|
691
729
|
|
|
692
730
|
@Tracked((_, v) => !v ? 'Status is required' : undefined)
|
|
@@ -776,13 +814,13 @@ for (const obj of tracker.trackedObjects) {
|
|
|
776
814
|
|
|
777
815
|
---
|
|
778
816
|
|
|
779
|
-
### `@
|
|
817
|
+
### `@AutoId`
|
|
780
818
|
|
|
781
|
-
Marks a
|
|
819
|
+
Marks a property as the server-assigned autoincrement primary key for this model. Works with both `TrackedObject` and `VersionedTrackedObject`. Only one `@AutoId` field is allowed per class. Enables the `beforeCommit` / `onCommit` lifecycle for placeholder ID management.
|
|
782
820
|
|
|
783
821
|
```typescript
|
|
784
822
|
class InvoiceModel extends TrackedObject {
|
|
785
|
-
@
|
|
823
|
+
@AutoId
|
|
786
824
|
id: number = 0;
|
|
787
825
|
|
|
788
826
|
@Tracked()
|
|
@@ -826,7 +864,7 @@ The placeholder counter never resets — each cycle continues from where it left
|
|
|
826
864
|
|
|
827
865
|
### `@Tracked()`
|
|
828
866
|
|
|
829
|
-
The property decorator. Intercepts every write, records an undo/redo pair, and optionally validates the new value. Works with
|
|
867
|
+
The property decorator. Intercepts every write, records an undo/redo pair, and optionally validates the new value. Works with `accessor` fields, explicit `get`/`set` pairs, and plain getters. Place it on the **accessor**, the **setter**, or the **getter**.
|
|
830
868
|
|
|
831
869
|
**With `accessor` (recommended):**
|
|
832
870
|
|
|
@@ -870,6 +908,42 @@ class ProductModel extends TrackedObject {
|
|
|
870
908
|
}
|
|
871
909
|
```
|
|
872
910
|
|
|
911
|
+
**With `get`/`set` and side effects** — decorate both getter and setter:
|
|
912
|
+
|
|
913
|
+
When the setter contains side-effect logic that must stay intact (e.g. cascading writes to other properties), decorate both the getter and the setter. The getter decoration registers `isEnabled` as a dependency source — any validator that reads it will automatically re-run when the setter fires. The setter decoration handles undo/redo as usual.
|
|
914
|
+
|
|
915
|
+
```typescript
|
|
916
|
+
class RuleModel extends TrackedObject {
|
|
917
|
+
private _isEnabled: boolean = false;
|
|
918
|
+
|
|
919
|
+
@Tracked()
|
|
920
|
+
get isEnabled(): boolean { return this._isEnabled; }
|
|
921
|
+
|
|
922
|
+
@Tracked()
|
|
923
|
+
set isEnabled(value: boolean) {
|
|
924
|
+
this._isEnabled = value;
|
|
925
|
+
if (value) {
|
|
926
|
+
this.scheduleDays = 'mon';
|
|
927
|
+
} else {
|
|
928
|
+
this.scheduleDays = '';
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
@Tracked((self: RuleModel, v) =>
|
|
933
|
+
self.isEnabled && !v ? 'Day is required' : undefined
|
|
934
|
+
)
|
|
935
|
+
accessor scheduleDays: string = '';
|
|
936
|
+
|
|
937
|
+
constructor(tracker: Tracker) {
|
|
938
|
+
super(tracker);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
When `isEnabled` is set to `true`, `scheduleDays`'s validator automatically re-runs because the getter declared the dependency. No manual `revalidate()` call is needed.
|
|
944
|
+
|
|
945
|
+
> Note: decorating just the getter (without the setter) is valid when the getter is purely computed — it registers the property as a dependency source without attaching any undo/redo logic.
|
|
946
|
+
|
|
873
947
|
**With a validator:**
|
|
874
948
|
|
|
875
949
|
The validator receives the model instance and the incoming value. Return an error string to fail, `undefined` to pass.
|
|
@@ -896,6 +970,8 @@ class OrderModel extends TrackedObject {
|
|
|
896
970
|
|
|
897
971
|
Validators are re-evaluated after every tracked write and after every undo/redo. Results are stored in `model.validationMessages` and rolled up into `tracker.isValid`.
|
|
898
972
|
|
|
973
|
+
Validators that read other properties automatically re-run when those properties change — this is handled by the dependency tracking mechanism (see [Dependency tracking](#dependency-tracking) in Concepts). For this to work, every property read inside a validator must be exposed through a `@Tracked`-decorated getter. `accessor` fields satisfy this automatically. For `get`/`set` pairs, both the getter and setter must be decorated with `@Tracked` — see the "getter + setter with side effects" example above.
|
|
974
|
+
|
|
899
975
|
**No-op detection**
|
|
900
976
|
|
|
901
977
|
Assigning the same value twice does not create an undo step and does not mark the model dirty. `null` and `undefined` are treated as equivalent to `''` for string properties.
|
package/dist/dev/index.cjs
CHANGED
|
@@ -20,7 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
|
|
23
|
+
AutoId: () => AutoId,
|
|
24
24
|
ObjectState: () => ObjectState,
|
|
25
25
|
OperationProperties: () => OperationProperties,
|
|
26
26
|
PropertyType: () => PropertyType,
|
|
@@ -127,17 +127,17 @@ var CollectionUtilities = class {
|
|
|
127
127
|
};
|
|
128
128
|
|
|
129
129
|
// src/ExternallyAssigned.ts
|
|
130
|
-
var
|
|
131
|
-
function
|
|
130
|
+
var AUTO_ID = /* @__PURE__ */ Symbol("autoId");
|
|
131
|
+
function AutoId(_target, context) {
|
|
132
132
|
context.addInitializer(function() {
|
|
133
|
-
Object.defineProperty(Object.getPrototypeOf(this),
|
|
133
|
+
Object.defineProperty(Object.getPrototypeOf(this), AUTO_ID, {
|
|
134
134
|
value: String(context.name),
|
|
135
135
|
configurable: true
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
|
-
function
|
|
140
|
-
return
|
|
139
|
+
function getAutoIdProperty(proto) {
|
|
140
|
+
return AUTO_ID in proto ? proto[AUTO_ID] : void 0;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// src/DependencyTracker.ts
|
|
@@ -476,7 +476,7 @@ var Tracker = class {
|
|
|
476
476
|
onCommit(keys) {
|
|
477
477
|
const lastOp = CollectionUtilities.getLast(this._undoOperations);
|
|
478
478
|
if (keys) {
|
|
479
|
-
this.trackedObjects.forEach((obj) => obj.
|
|
479
|
+
this.trackedObjects.forEach((obj) => obj.applyServerIds(keys, lastOp));
|
|
480
480
|
}
|
|
481
481
|
this.trackedObjects.forEach((obj) => obj.onCommitted(lastOp));
|
|
482
482
|
this._commitStateOperation = lastOp;
|
|
@@ -487,7 +487,7 @@ var Tracker = class {
|
|
|
487
487
|
}
|
|
488
488
|
beforeCommit() {
|
|
489
489
|
this.trackedObjects.forEach((model) => {
|
|
490
|
-
const propertyName =
|
|
490
|
+
const propertyName = getAutoIdProperty(
|
|
491
491
|
Object.getPrototypeOf(model)
|
|
492
492
|
);
|
|
493
493
|
if (propertyName && model[propertyName] <= 0) {
|
|
@@ -626,8 +626,8 @@ var TrackedObjectBase = class {
|
|
|
626
626
|
set dirtyCounter(value) {
|
|
627
627
|
this._dirtyCounter = value;
|
|
628
628
|
}
|
|
629
|
-
|
|
630
|
-
const propertyName =
|
|
629
|
+
applyServerIds(keys, lastOp) {
|
|
630
|
+
const propertyName = getAutoIdProperty(Object.getPrototypeOf(this));
|
|
631
631
|
if (!propertyName || this[propertyName] >= 0) return;
|
|
632
632
|
const response = keys.find((x) => x.placeholder === this[propertyName]);
|
|
633
633
|
if (!response) return;
|
|
@@ -760,10 +760,10 @@ var VersionedTrackedObject = class extends TrackedObjectBase {
|
|
|
760
760
|
}
|
|
761
761
|
this.dirtyCounter = 0;
|
|
762
762
|
}
|
|
763
|
-
|
|
764
|
-
const propertyName =
|
|
763
|
+
applyServerIds(keys, lastOp) {
|
|
764
|
+
const propertyName = getAutoIdProperty(Object.getPrototypeOf(this));
|
|
765
765
|
const response = propertyName && this[propertyName] < 0 ? keys.find((x) => x.placeholder === this[propertyName]) : void 0;
|
|
766
|
-
super.
|
|
766
|
+
super.applyServerIds(keys, lastOp);
|
|
767
767
|
if (!response) return;
|
|
768
768
|
const newValue = response.value;
|
|
769
769
|
const redoFn = () => {
|
|
@@ -795,7 +795,7 @@ var VersionedTrackedObject = class extends TrackedObjectBase {
|
|
|
795
795
|
const prev = this._committedState;
|
|
796
796
|
const isNeverPersisted = prev === "New" /* New */ || prev === "InsertReverted" /* InsertReverted */;
|
|
797
797
|
const target = isNeverPersisted ? "Unchanged" /* Unchanged */ : "Deleted" /* Deleted */;
|
|
798
|
-
const propertyName =
|
|
798
|
+
const propertyName = getAutoIdProperty(Object.getPrototypeOf(this));
|
|
799
799
|
const prevId = propertyName !== void 0 ? this[propertyName] : void 0;
|
|
800
800
|
const prevPendingHardDeletes = new Set(this.pendingHardDeletes);
|
|
801
801
|
const prevDirtyCounter = this.dirtyCounter;
|
|
@@ -847,6 +847,13 @@ var VersionedTrackedObject = class extends TrackedObjectBase {
|
|
|
847
847
|
function Tracked(validator, options) {
|
|
848
848
|
function decorator(target, context) {
|
|
849
849
|
const propertyName = String(context.name);
|
|
850
|
+
if (context.kind === "getter") {
|
|
851
|
+
const getterFn = target;
|
|
852
|
+
return function() {
|
|
853
|
+
DependencyTracker.record(this, propertyName);
|
|
854
|
+
return getterFn.call(this);
|
|
855
|
+
};
|
|
856
|
+
}
|
|
850
857
|
if (context.kind === "accessor") {
|
|
851
858
|
const accessorTarget = target;
|
|
852
859
|
if (validator) {
|
|
@@ -1288,7 +1295,7 @@ var TrackedCollectionChanged = class {
|
|
|
1288
1295
|
};
|
|
1289
1296
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1290
1297
|
0 && (module.exports = {
|
|
1291
|
-
|
|
1298
|
+
AutoId,
|
|
1292
1299
|
ObjectState,
|
|
1293
1300
|
OperationProperties,
|
|
1294
1301
|
PropertyType,
|