@katn30/trakr 3.0.0 → 4.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.
- package/README.md +70 -0
- package/dist/dev/index.cjs +597 -63
- package/dist/dev/index.cjs.map +1 -1
- package/dist/dev/index.d.cts +65 -5
- package/dist/dev/index.d.ts +65 -5
- package/dist/dev/index.js +592 -62
- package/dist/dev/index.js.map +1 -1
- package/dist/prod/index.cjs +597 -63
- package/dist/prod/index.cjs.map +1 -1
- package/dist/prod/index.js +592 -62
- package/dist/prod/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1579,6 +1579,76 @@ Given `issue.stage = 'InAnalysis'` and `issue.analysisSummary = 'AS'`, `generate
|
|
|
1579
1579
|
|
|
1580
1580
|
Subclass `@EventTracked` fields appear **after** base-class fields, matching the declaration order across the prototype chain.
|
|
1581
1581
|
|
|
1582
|
+
### Scalars vs. sequences: choosing the primitive
|
|
1583
|
+
|
|
1584
|
+
`@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.
|
|
1585
|
+
|
|
1586
|
+
**`@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.
|
|
1587
|
+
|
|
1588
|
+
**`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.
|
|
1589
|
+
|
|
1590
|
+
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.
|
|
1591
|
+
|
|
1592
|
+
```typescript
|
|
1593
|
+
// WRONG — accessor collapses "Submitted → InAnalysis → InFixing" into one event
|
|
1594
|
+
class IssueModel extends TrackedObject {
|
|
1595
|
+
@EventTracked(IssueEvents.StageTransitioned) accessor stage: string = 'Submitted';
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
issue.stage = 'InAnalysis';
|
|
1599
|
+
issue.stage = 'InFixing';
|
|
1600
|
+
|
|
1601
|
+
// generateEvents() returns ONE event with the final value:
|
|
1602
|
+
// [{ eventType: 'StageTransitioned', payload: { stage: 'InFixing' } }]
|
|
1603
|
+
// Backend replays: Submitted → InFixing, rejects as illegal transition.
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
```typescript
|
|
1607
|
+
// RIGHT — one StageTransitioned event per push, in order
|
|
1608
|
+
class StageTransition extends TrackedObject {
|
|
1609
|
+
@EventTracked(IssueEvents.StageTransitioned) accessor stage: string;
|
|
1610
|
+
transitionedAt: string;
|
|
1611
|
+
transitionedBy: string | null;
|
|
1612
|
+
|
|
1613
|
+
constructor(t: Tracker, stage: string, at: string, by: string | null) {
|
|
1614
|
+
super(t);
|
|
1615
|
+
this.stage = stage;
|
|
1616
|
+
this.transitionedAt = at;
|
|
1617
|
+
this.transitionedBy = by;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
class IssueModel extends TrackedObject {
|
|
1622
|
+
stage: string = 'Submitted'; // plain field, drives UI only
|
|
1623
|
+
readonly transitions: EventTrackedCollection<StageTransition>;
|
|
1624
|
+
|
|
1625
|
+
constructor(t: Tracker) {
|
|
1626
|
+
super(t);
|
|
1627
|
+
this.transitions = new EventTrackedCollection<StageTransition>(
|
|
1628
|
+
t, [], undefined, { itemAdded: IssueEvents.StageTransitioned },
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
transitionTo(target: string, by: string | null): void {
|
|
1633
|
+
this.stage = target;
|
|
1634
|
+
this.transitions.push(
|
|
1635
|
+
this.tracker.construct(() => new StageTransition(this.tracker, target, new Date().toISOString(), by)),
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
issue.transitionTo('InAnalysis', 'alice');
|
|
1641
|
+
issue.transitionTo('InFixing', 'alice');
|
|
1642
|
+
|
|
1643
|
+
// generateEvents() returns TWO events, in insertion order:
|
|
1644
|
+
// [
|
|
1645
|
+
// { eventType: 'StageTransitioned', payload: { stage: 'InAnalysis' }, ... },
|
|
1646
|
+
// { eventType: 'StageTransitioned', payload: { stage: 'InFixing' }, ... },
|
|
1647
|
+
// ]
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
**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.
|
|
1651
|
+
|
|
1582
1652
|
### Migration
|
|
1583
1653
|
|
|
1584
1654
|
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.
|