@katn30/trakr 3.0.0 → 4.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 +128 -4
- package/dist/dev/index.cjs +600 -63
- package/dist/dev/index.cjs.map +1 -1
- package/dist/dev/index.d.cts +71 -10
- package/dist/dev/index.d.ts +71 -10
- package/dist/dev/index.js +595 -62
- package/dist/dev/index.js.map +1 -1
- package/dist/prod/index.cjs +600 -63
- package/dist/prod/index.cjs.map +1 -1
- package/dist/prod/index.js +595 -62
- package/dist/prod/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -610,6 +610,14 @@ tracker.onCommit(keys); // same, plus write real server IDs to @AutoId fie
|
|
|
610
610
|
2. Transitions every tracked object's `state` to `Unchanged` and resets `dirtyCounter`.
|
|
611
611
|
3. Appends the state change into the existing last undo operation — so undo atomically reverts both the user's edits and the committed state together (no spurious extra undo steps).
|
|
612
612
|
|
|
613
|
+
**Lookup by trackingId**
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
const obj = tracker.getByTrackingId(42); // TrackedObject | undefined
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
Returns the tracked object whose `trackingId` matches the given value, or `undefined` if none. Deleted objects are still findable this way (they remain in `trackedObjects` until `destroy()`). Useful for debugging, correlating server responses against known objects, or hydrating a UI selection from a persisted `trackingId`.
|
|
620
|
+
|
|
613
621
|
**Sessions**
|
|
614
622
|
|
|
615
623
|
```typescript
|
|
@@ -886,6 +894,8 @@ tracker.onCommit(response.ids);
|
|
|
886
894
|
|
|
887
895
|
Marks a property as the server-assigned autoincrement primary key for this model. Only one `@AutoId` field is allowed per class. Enables the `onCommit` lifecycle for real-ID assignment.
|
|
888
896
|
|
|
897
|
+
`@AutoId` is a **specialisation of `@Id`**: it also registers the property as part of the model's identity (so `getIdentity(obj)` includes it), but it additionally singles the property out as the one trakr will overwrite from `onCommit(keys)`. Use `@Id` for identity properties you assign yourself; use `@AutoId` for the single property trakr should patch on commit. See [`@Id`](#id) for identity-only properties (composite keys, caller-assigned UUIDs, natural keys).
|
|
898
|
+
|
|
889
899
|
```typescript
|
|
890
900
|
class InvoiceModel extends TrackedObject {
|
|
891
901
|
@AutoId
|
|
@@ -927,6 +937,45 @@ tracker.onCommit(serverIds);
|
|
|
927
937
|
|
|
928
938
|
`trackingId` values are globally unique across the lifetime of the tracker and never reused, so they can safely serve as correlation keys across multiple save cycles.
|
|
929
939
|
|
|
940
|
+
**Reactivity gate.** The `@AutoId` write performed by `onCommit(keys)` is a **library-internal write, not a user edit**. It does **not** fire `TrackedObject.changed` for the `@AutoId` property, does **not** bump `dirtyCounter`, does **not** re-run `@Tracked` validators, and does **not** flicker `tracker.isDirty` back to `true` during commit. After `onCommit` returns, `state === Unchanged` and `isDirty === false` — as if the object were freshly loaded with the real PK. Treat the write as *authoritative baseline update*, not as a change event.
|
|
941
|
+
|
|
942
|
+
---
|
|
943
|
+
|
|
944
|
+
### `@Id`
|
|
945
|
+
|
|
946
|
+
Marks a property as part of the model's **caller-provided identity**. Any type is allowed (string, number, UUID, ULID, tuple-like composite via multiple decorators). Trakr never mutates the value — you set it (typically from the server, or client-generated for UUID schemas), and trakr uses it purely to compute `getIdentity(obj)`, `getIdentityObject(obj)`, and `getIdentityProperties(proto)`.
|
|
947
|
+
|
|
948
|
+
```typescript
|
|
949
|
+
class TenantModel extends TrackedObject {
|
|
950
|
+
@Id
|
|
951
|
+
code: string = ''; // caller-assigned string PK
|
|
952
|
+
|
|
953
|
+
@Tracked()
|
|
954
|
+
accessor name: string = '';
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
class LineItemModel extends TrackedObject {
|
|
958
|
+
@Id
|
|
959
|
+
orderId: number = 0; // composite key part 1
|
|
960
|
+
@Id
|
|
961
|
+
lineNo: number = 0; // composite key part 2
|
|
962
|
+
|
|
963
|
+
@Tracked()
|
|
964
|
+
accessor qty: number = 0;
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
**Relationship to `@AutoId`.**
|
|
969
|
+
|
|
970
|
+
| | `@Id` | `@AutoId` |
|
|
971
|
+
|----------------------------------|-----------------------------------|-------------------------------------------------|
|
|
972
|
+
| Registers property as identity | yes | yes (implicitly) |
|
|
973
|
+
| Value type | any | any (via `IdAssignment<V>`; historically `number`) |
|
|
974
|
+
| Written by `onCommit(keys)` | never | yes, from the matching `IdAssignment.value` |
|
|
975
|
+
| Multiple per class | yes (composite key) | no (at most one) |
|
|
976
|
+
|
|
977
|
+
Use `@Id` for identity you own (client-generated UUIDs, natural keys, composite keys). Use `@AutoId` for the single property whose value only the server can produce and that must be patched back into the model after `onCommit`. A class may combine them: several `@Id` properties for a composite key plus one `@AutoId` for a surrogate PK, if that matches your schema.
|
|
978
|
+
|
|
930
979
|
---
|
|
931
980
|
|
|
932
981
|
### `ITracked`
|
|
@@ -951,19 +1000,24 @@ function isReady(item: ITracked): boolean {
|
|
|
951
1000
|
|
|
952
1001
|
---
|
|
953
1002
|
|
|
954
|
-
### `IdAssignment
|
|
1003
|
+
### `IdAssignment<V>`
|
|
955
1004
|
|
|
956
|
-
The shape of each entry in the `keys` array passed to `tracker.onCommit(keys)`:
|
|
1005
|
+
The shape of each entry in the `keys` array passed to `tracker.onCommit(keys)`. Generic in the PK value type, defaulting to `number`:
|
|
957
1006
|
|
|
958
1007
|
```typescript
|
|
959
1008
|
import type { IdAssignment } from '@katn30/trakr';
|
|
960
|
-
// { trackingId: number; value:
|
|
1009
|
+
// IdAssignment<V = number> = { trackingId: number; value: V }
|
|
1010
|
+
|
|
1011
|
+
const numericKeys: IdAssignment[] = [{ trackingId: 1, value: 42 }];
|
|
1012
|
+
const uuidKeys: IdAssignment<string>[] = [{ trackingId: 1, value: '01HXYZ-ULID' }];
|
|
961
1013
|
```
|
|
962
1014
|
|
|
963
1015
|
| Field | Type | Description |
|
|
964
1016
|
|---|---|---|
|
|
965
1017
|
| `trackingId` | `number` | The `trackingId` of the object that received a new server-assigned PK |
|
|
966
|
-
| `value` | `number` | The real server-assigned ID to write to the `@AutoId` field |
|
|
1018
|
+
| `value` | `V` (default `number`) | The real server-assigned ID to write to the `@AutoId` field |
|
|
1019
|
+
|
|
1020
|
+
`tracker.onCommit<V>(keys)` is generic in `V`: pass `IdAssignment<string>[]` for UUID/ULID schemas, `IdAssignment[]` for the numeric default. Trakr writes `entry.value` straight into the `@AutoId` field — the field's declared type is what enforces the match at the call site (e.g. `@AutoId id: string = ''` with `onCommit<string>(...)`).
|
|
967
1021
|
|
|
968
1022
|
The server returns one `IdAssignment` per item that produced a new database row — both inserted objects and, in temporal tables, updated objects (see [Temporally versioned tables](#temporally-versioned-tables)). `onCommit()` iterates every entry, matches by `trackingId` against every tracked object, and writes `value` to the `@AutoId` field of any match.
|
|
969
1023
|
|
|
@@ -1579,6 +1633,76 @@ Given `issue.stage = 'InAnalysis'` and `issue.analysisSummary = 'AS'`, `generate
|
|
|
1579
1633
|
|
|
1580
1634
|
Subclass `@EventTracked` fields appear **after** base-class fields, matching the declaration order across the prototype chain.
|
|
1581
1635
|
|
|
1636
|
+
### Scalars vs. sequences: choosing the primitive
|
|
1637
|
+
|
|
1638
|
+
`@EventTracked` and `EventTrackedCollection` look interchangeable when both can carry the same field on the wire — a `stage` property could sit on the model as either an accessor or an item in a collection. They are **not** interchangeable: they encode different semantics, and picking the wrong one silently loses information at Save time.
|
|
1639
|
+
|
|
1640
|
+
**`@EventTracked` accessor — the field is a *value*.** Multiple writes in one session collapse to **one** event carrying the field's **final** value. That is correct behaviour: consumers care about "what the field is now", not "how many times the user retyped it". Typing `Faulty widget` character by character produces one `SubmittedDetailsRevised{name: 'Faulty widget'}`, not eleven.
|
|
1641
|
+
|
|
1642
|
+
**`EventTrackedCollection` — the field is a *sequence of actions*.** Each push emits its own `itemAdded` event, in insertion order. Two pushes produce two events; they never collapse.
|
|
1643
|
+
|
|
1644
|
+
The trap: state-machine-style fields *look* like scalars ("current stage"), so it is tempting to model them as `@EventTracked accessor stage`. That is wrong. A transition is not an update to a value — it is a discrete action, and every intermediate state must be persisted for the backend's transition preconditions (`from → to` allowed?) to hold on replay.
|
|
1645
|
+
|
|
1646
|
+
```typescript
|
|
1647
|
+
// WRONG — accessor collapses "Submitted → InAnalysis → InFixing" into one event
|
|
1648
|
+
class IssueModel extends TrackedObject {
|
|
1649
|
+
@EventTracked(IssueEvents.StageTransitioned) accessor stage: string = 'Submitted';
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
issue.stage = 'InAnalysis';
|
|
1653
|
+
issue.stage = 'InFixing';
|
|
1654
|
+
|
|
1655
|
+
// generateEvents() returns ONE event with the final value:
|
|
1656
|
+
// [{ eventType: 'StageTransitioned', payload: { stage: 'InFixing' } }]
|
|
1657
|
+
// Backend replays: Submitted → InFixing, rejects as illegal transition.
|
|
1658
|
+
```
|
|
1659
|
+
|
|
1660
|
+
```typescript
|
|
1661
|
+
// RIGHT — one StageTransitioned event per push, in order
|
|
1662
|
+
class StageTransition extends TrackedObject {
|
|
1663
|
+
@EventTracked(IssueEvents.StageTransitioned) accessor stage: string;
|
|
1664
|
+
transitionedAt: string;
|
|
1665
|
+
transitionedBy: string | null;
|
|
1666
|
+
|
|
1667
|
+
constructor(t: Tracker, stage: string, at: string, by: string | null) {
|
|
1668
|
+
super(t);
|
|
1669
|
+
this.stage = stage;
|
|
1670
|
+
this.transitionedAt = at;
|
|
1671
|
+
this.transitionedBy = by;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
class IssueModel extends TrackedObject {
|
|
1676
|
+
stage: string = 'Submitted'; // plain field, drives UI only
|
|
1677
|
+
readonly transitions: EventTrackedCollection<StageTransition>;
|
|
1678
|
+
|
|
1679
|
+
constructor(t: Tracker) {
|
|
1680
|
+
super(t);
|
|
1681
|
+
this.transitions = new EventTrackedCollection<StageTransition>(
|
|
1682
|
+
t, [], undefined, { itemAdded: IssueEvents.StageTransitioned },
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
transitionTo(target: string, by: string | null): void {
|
|
1687
|
+
this.stage = target;
|
|
1688
|
+
this.transitions.push(
|
|
1689
|
+
this.tracker.construct(() => new StageTransition(this.tracker, target, new Date().toISOString(), by)),
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
issue.transitionTo('InAnalysis', 'alice');
|
|
1695
|
+
issue.transitionTo('InFixing', 'alice');
|
|
1696
|
+
|
|
1697
|
+
// generateEvents() returns TWO events, in insertion order:
|
|
1698
|
+
// [
|
|
1699
|
+
// { eventType: 'StageTransitioned', payload: { stage: 'InAnalysis' }, ... },
|
|
1700
|
+
// { eventType: 'StageTransitioned', payload: { stage: 'InFixing' }, ... },
|
|
1701
|
+
// ]
|
|
1702
|
+
```
|
|
1703
|
+
|
|
1704
|
+
**Rule of thumb.** If the field's meaning is *"what it is now"* and the backend does not need to see every intermediate write, use `@EventTracked` on an accessor. If each write is a *distinct, ordered action* the backend must apply sequentially — state transitions, audit log entries, phase changes, workflow steps — use `EventTrackedCollection` with an `itemAdded` tag.
|
|
1705
|
+
|
|
1582
1706
|
### Migration
|
|
1583
1707
|
|
|
1584
1708
|
There is **nothing to migrate** from v2 → v3 for existing code. `Tracker`, `TrackedObject`, `TrackedCollection`, `@Tracked`, and `@AutoId` are unchanged. Opt in per class or per collection whenever the consumer needs event generation. A single tracker instance can mix event-tracked and non-event-tracked objects.
|