trakked 1.2.0 → 2.1.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 +76 -10
- 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:
|
|
@@ -467,12 +495,12 @@ Use this instead of `TrackedObject` when your database is **versioned (temporal)
|
|
|
467
495
|
import {
|
|
468
496
|
VersionedTrackedObject,
|
|
469
497
|
VersionedObjectState,
|
|
470
|
-
|
|
498
|
+
AutoId,
|
|
471
499
|
Tracked,
|
|
472
500
|
} from 'trakked';
|
|
473
501
|
|
|
474
502
|
class OrderModel extends VersionedTrackedObject {
|
|
475
|
-
@
|
|
503
|
+
@AutoId
|
|
476
504
|
id: number = 0;
|
|
477
505
|
|
|
478
506
|
@Tracked()
|
|
@@ -522,7 +550,7 @@ Objects default to `Unchanged`. Set properties inside the constructor — they a
|
|
|
522
550
|
|
|
523
551
|
```typescript
|
|
524
552
|
class OrderModel extends VersionedTrackedObject {
|
|
525
|
-
@
|
|
553
|
+
@AutoId id: number = 0;
|
|
526
554
|
@Tracked() accessor description: string = '';
|
|
527
555
|
constructor(tracker: Tracker, data?: { id: number; description: string }) {
|
|
528
556
|
super(tracker); // initialState defaults to Unchanged
|
|
@@ -664,14 +692,14 @@ import {
|
|
|
664
692
|
VersionedTrackedObject,
|
|
665
693
|
VersionedObjectState,
|
|
666
694
|
Tracked,
|
|
667
|
-
|
|
695
|
+
AutoId,
|
|
668
696
|
TrackedCollection,
|
|
669
697
|
} from 'trakked';
|
|
670
698
|
|
|
671
699
|
const tracker = new Tracker();
|
|
672
700
|
|
|
673
701
|
class OrderLine extends VersionedTrackedObject {
|
|
674
|
-
@
|
|
702
|
+
@AutoId
|
|
675
703
|
id: number = 0;
|
|
676
704
|
|
|
677
705
|
@Tracked((_, v) => !v ? 'Description is required' : undefined)
|
|
@@ -686,7 +714,7 @@ class OrderLine extends VersionedTrackedObject {
|
|
|
686
714
|
}
|
|
687
715
|
|
|
688
716
|
class OrderModel extends VersionedTrackedObject {
|
|
689
|
-
@
|
|
717
|
+
@AutoId
|
|
690
718
|
id: number = 0;
|
|
691
719
|
|
|
692
720
|
@Tracked((_, v) => !v ? 'Status is required' : undefined)
|
|
@@ -776,13 +804,13 @@ for (const obj of tracker.trackedObjects) {
|
|
|
776
804
|
|
|
777
805
|
---
|
|
778
806
|
|
|
779
|
-
### `@
|
|
807
|
+
### `@AutoId`
|
|
780
808
|
|
|
781
|
-
Marks a
|
|
809
|
+
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
810
|
|
|
783
811
|
```typescript
|
|
784
812
|
class InvoiceModel extends TrackedObject {
|
|
785
|
-
@
|
|
813
|
+
@AutoId
|
|
786
814
|
id: number = 0;
|
|
787
815
|
|
|
788
816
|
@Tracked()
|
|
@@ -826,7 +854,7 @@ The placeholder counter never resets — each cycle continues from where it left
|
|
|
826
854
|
|
|
827
855
|
### `@Tracked()`
|
|
828
856
|
|
|
829
|
-
The property decorator. Intercepts every write, records an undo/redo pair, and optionally validates the new value. Works with
|
|
857
|
+
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
858
|
|
|
831
859
|
**With `accessor` (recommended):**
|
|
832
860
|
|
|
@@ -870,6 +898,42 @@ class ProductModel extends TrackedObject {
|
|
|
870
898
|
}
|
|
871
899
|
```
|
|
872
900
|
|
|
901
|
+
**With `get`/`set` and side effects** — decorate both getter and setter:
|
|
902
|
+
|
|
903
|
+
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.
|
|
904
|
+
|
|
905
|
+
```typescript
|
|
906
|
+
class RuleModel extends TrackedObject {
|
|
907
|
+
private _isEnabled: boolean = false;
|
|
908
|
+
|
|
909
|
+
@Tracked()
|
|
910
|
+
get isEnabled(): boolean { return this._isEnabled; }
|
|
911
|
+
|
|
912
|
+
@Tracked()
|
|
913
|
+
set isEnabled(value: boolean) {
|
|
914
|
+
this._isEnabled = value;
|
|
915
|
+
if (value) {
|
|
916
|
+
this.scheduleDays = 'mon';
|
|
917
|
+
} else {
|
|
918
|
+
this.scheduleDays = '';
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
@Tracked((self: RuleModel, v) =>
|
|
923
|
+
self.isEnabled && !v ? 'Day is required' : undefined
|
|
924
|
+
)
|
|
925
|
+
accessor scheduleDays: string = '';
|
|
926
|
+
|
|
927
|
+
constructor(tracker: Tracker) {
|
|
928
|
+
super(tracker);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
When `isEnabled` is set to `true`, `scheduleDays`'s validator automatically re-runs because the getter declared the dependency. No manual `revalidate()` call is needed.
|
|
934
|
+
|
|
935
|
+
> 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.
|
|
936
|
+
|
|
873
937
|
**With a validator:**
|
|
874
938
|
|
|
875
939
|
The validator receives the model instance and the incoming value. Return an error string to fail, `undefined` to pass.
|
|
@@ -896,6 +960,8 @@ class OrderModel extends TrackedObject {
|
|
|
896
960
|
|
|
897
961
|
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
962
|
|
|
963
|
+
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.
|
|
964
|
+
|
|
899
965
|
**No-op detection**
|
|
900
966
|
|
|
901
967
|
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,
|