@katn30/trakr 4.0.0 → 4.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 +85 -7
- package/dist/dev/index.cjs +3 -0
- package/dist/dev/index.cjs.map +1 -1
- package/dist/dev/index.d.cts +6 -5
- package/dist/dev/index.d.ts +6 -5
- package/dist/dev/index.js +3 -0
- package/dist/dev/index.js.map +1 -1
- package/dist/prod/index.cjs +3 -0
- package/dist/prod/index.cjs.map +1 -1
- package/dist/prod/index.js +3 -0
- package/dist/prod/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -463,13 +463,15 @@ Adding or removing a `TrackedObject` from a `TrackedCollection` transitions its
|
|
|
463
463
|
```typescript
|
|
464
464
|
const item = tracker.construct(() => new ItemModel(tracker)); // Unchanged
|
|
465
465
|
items.push(item); // → Insert
|
|
466
|
-
items.remove(item); // → Unchanged (was never saved)
|
|
466
|
+
items.remove(item); // → Unchanged, and untracked (was never saved)
|
|
467
467
|
|
|
468
468
|
const loaded = tracker.construct(() => new ItemModel(tracker, { id: 1 })); // Unchanged
|
|
469
469
|
items.push(loaded); // → Insert
|
|
470
470
|
items.remove(loaded); // → Deleted
|
|
471
471
|
```
|
|
472
472
|
|
|
473
|
+
Removing an item whose state is `Insert` collapses it as if it had never existed: the state resets to `Unchanged` and the object is removed from `tracker.trackedObjects` in the same undo step. You do not need to call `destroy()` yourself — the collection handles it. Undo of the remove re-tracks the item and puts it back in the collection at state `Insert`.
|
|
474
|
+
|
|
473
475
|
**Via @Tracked property**
|
|
474
476
|
|
|
475
477
|
When a `@Tracked` property holds a `TrackedObject` value, assigning to it has the same effect: the outgoing value transitions to `Deleted` (or `Unchanged` if it was `Insert`), and the incoming value transitions to `Insert`:
|
|
@@ -536,7 +538,7 @@ There is no separate redo transition. Redo simply re-runs the original `do` acti
|
|
|
536
538
|
|
|
537
539
|
#### Key notes
|
|
538
540
|
|
|
539
|
-
**`removed/do` from `Insert` collapses to `Unchanged
|
|
541
|
+
**`removed/do` from `Insert` collapses to `Unchanged` and untracks the object** — if an object was added and then removed before ever being committed, it was never persisted. The transition resets `dirtyCounter` to zero, removes the object from `tracker.trackedObjects`, and clears its dependency-tracking entries — as if the add never happened. Nothing needs to be sent to the server, and the object no longer contributes to `tracker.isValid`. Undo of the remove re-tracks the object and restores its previous validity accounting.
|
|
540
542
|
|
|
541
543
|
**`committed/undo` reverses the server operation** — undoing past a commit puts the object into the state that requires the inverse server operation. Undoing a committed INSERT requires a DELETE; undoing a committed DELETE requires a new INSERT; undoing a committed UPDATE requires another UPDATE with the pre-edit values.
|
|
542
544
|
|
|
@@ -610,6 +612,14 @@ tracker.onCommit(keys); // same, plus write real server IDs to @AutoId fie
|
|
|
610
612
|
2. Transitions every tracked object's `state` to `Unchanged` and resets `dirtyCounter`.
|
|
611
613
|
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
614
|
|
|
615
|
+
**Lookup by trackingId**
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
const obj = tracker.getByTrackingId(42); // TrackedObject | undefined
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
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`.
|
|
622
|
+
|
|
613
623
|
**Sessions**
|
|
614
624
|
|
|
615
625
|
```typescript
|
|
@@ -886,6 +896,8 @@ tracker.onCommit(response.ids);
|
|
|
886
896
|
|
|
887
897
|
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
898
|
|
|
899
|
+
`@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).
|
|
900
|
+
|
|
889
901
|
```typescript
|
|
890
902
|
class InvoiceModel extends TrackedObject {
|
|
891
903
|
@AutoId
|
|
@@ -927,6 +939,45 @@ tracker.onCommit(serverIds);
|
|
|
927
939
|
|
|
928
940
|
`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
941
|
|
|
942
|
+
**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.
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
### `@Id`
|
|
947
|
+
|
|
948
|
+
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)`.
|
|
949
|
+
|
|
950
|
+
```typescript
|
|
951
|
+
class TenantModel extends TrackedObject {
|
|
952
|
+
@Id
|
|
953
|
+
code: string = ''; // caller-assigned string PK
|
|
954
|
+
|
|
955
|
+
@Tracked()
|
|
956
|
+
accessor name: string = '';
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
class LineItemModel extends TrackedObject {
|
|
960
|
+
@Id
|
|
961
|
+
orderId: number = 0; // composite key part 1
|
|
962
|
+
@Id
|
|
963
|
+
lineNo: number = 0; // composite key part 2
|
|
964
|
+
|
|
965
|
+
@Tracked()
|
|
966
|
+
accessor qty: number = 0;
|
|
967
|
+
}
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
**Relationship to `@AutoId`.**
|
|
971
|
+
|
|
972
|
+
| | `@Id` | `@AutoId` |
|
|
973
|
+
|----------------------------------|-----------------------------------|-------------------------------------------------|
|
|
974
|
+
| Registers property as identity | yes | yes (implicitly) |
|
|
975
|
+
| Value type | any | any (via `IdAssignment<V>`; historically `number`) |
|
|
976
|
+
| Written by `onCommit(keys)` | never | yes, from the matching `IdAssignment.value` |
|
|
977
|
+
| Multiple per class | yes (composite key) | no (at most one) |
|
|
978
|
+
|
|
979
|
+
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.
|
|
980
|
+
|
|
930
981
|
---
|
|
931
982
|
|
|
932
983
|
### `ITracked`
|
|
@@ -951,19 +1002,24 @@ function isReady(item: ITracked): boolean {
|
|
|
951
1002
|
|
|
952
1003
|
---
|
|
953
1004
|
|
|
954
|
-
### `IdAssignment
|
|
1005
|
+
### `IdAssignment<V>`
|
|
955
1006
|
|
|
956
|
-
The shape of each entry in the `keys` array passed to `tracker.onCommit(keys)`:
|
|
1007
|
+
The shape of each entry in the `keys` array passed to `tracker.onCommit(keys)`. Generic in the PK value type, defaulting to `number`:
|
|
957
1008
|
|
|
958
1009
|
```typescript
|
|
959
1010
|
import type { IdAssignment } from '@katn30/trakr';
|
|
960
|
-
// { trackingId: number; value:
|
|
1011
|
+
// IdAssignment<V = number> = { trackingId: number; value: V }
|
|
1012
|
+
|
|
1013
|
+
const numericKeys: IdAssignment[] = [{ trackingId: 1, value: 42 }];
|
|
1014
|
+
const uuidKeys: IdAssignment<string>[] = [{ trackingId: 1, value: '01HXYZ-ULID' }];
|
|
961
1015
|
```
|
|
962
1016
|
|
|
963
1017
|
| Field | Type | Description |
|
|
964
1018
|
|---|---|---|
|
|
965
1019
|
| `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 |
|
|
1020
|
+
| `value` | `V` (default `number`) | The real server-assigned ID to write to the `@AutoId` field |
|
|
1021
|
+
|
|
1022
|
+
`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
1023
|
|
|
968
1024
|
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
1025
|
|
|
@@ -1136,7 +1192,7 @@ accessor status: string = '';
|
|
|
1136
1192
|
|
|
1137
1193
|
### `TrackedCollection<T>`
|
|
1138
1194
|
|
|
1139
|
-
A
|
|
1195
|
+
A tracked collection with a full array-shaped API. All mutation methods are recorded and undoable. Implements `Array<T>` for type-level interop with array-consuming APIs — with one runtime gap, described in **Positional access at runtime** below.
|
|
1140
1196
|
|
|
1141
1197
|
```typescript
|
|
1142
1198
|
const items = new TrackedCollection<string>(tracker);
|
|
@@ -1152,6 +1208,28 @@ const items = new TrackedCollection<string>(
|
|
|
1152
1208
|
);
|
|
1153
1209
|
```
|
|
1154
1210
|
|
|
1211
|
+
**Positional access at runtime**
|
|
1212
|
+
|
|
1213
|
+
`TrackedCollection<T>` declares the `[n: number]: T` index signature (inherited from `implements Array<T>`), but numeric-index access is **not** wired up at runtime. TypeScript accepts `col[n]` and `col[n] = x` without complaint, but neither behaves like a real array:
|
|
1214
|
+
|
|
1215
|
+
- `col[0]` returns `undefined` at runtime, even when `col.length > 0`.
|
|
1216
|
+
- `col[0] = x` does not mutate the collection and does not trigger tracking. It silently sets a data property on the class instance that shadows the (unimplemented) indexer — subsequent `col[0]` reads will return `x` while `col.length`, iteration, and `col.collection` still reflect the real state. This divergence will not surface as an error; it will surface as inconsistent UI or a save payload missing the write.
|
|
1217
|
+
|
|
1218
|
+
The interface is kept for interop (passing a `TrackedCollection` to code typed against `Array<T>`), but the runtime backing is deliberately not provided: wiring it up requires either a `Proxy` (breaks instance identity across the tracker API and adds an indirection layer on every property access) or per-index `defineProperty` accessors (allocates one closure per element, and numeric properties become enumerable and appear in `Object.keys` and `for...in`). Neither trade-off is worth it for a pattern the class already has first-class alternatives for.
|
|
1219
|
+
|
|
1220
|
+
Use these instead:
|
|
1221
|
+
|
|
1222
|
+
| Instead of | Use |
|
|
1223
|
+
|---|---|
|
|
1224
|
+
| `col[n]` (read) | `col.at(n)` — participates in dependency tracking |
|
|
1225
|
+
| — | `col.collection[n]` — direct read of the underlying array, no dependency tracking |
|
|
1226
|
+
| `col[n] = x` (write) | `col.replaceAt(n, x)` — tracked, undoable |
|
|
1227
|
+
| — | `col.splice(n, 1, x)` — tracked, undoable |
|
|
1228
|
+
| `for (let i = 0; i < col.length; i++) col[i]` | `for (const item of col) ...` |
|
|
1229
|
+
| — | `col.forEach(...)`, `col.map(...)`, etc. |
|
|
1230
|
+
|
|
1231
|
+
When passing a `TrackedCollection` to a third-party function typed against `Array<T>` that reads by index internally, pass `col.collection` instead. It exposes the underlying `T[]` with zero copying. Treat it as read-only: any mutation applied to that reference bypasses the tracker entirely.
|
|
1232
|
+
|
|
1155
1233
|
**Tracked mutation methods**
|
|
1156
1234
|
|
|
1157
1235
|
All of these create undo steps:
|
package/dist/dev/index.cjs
CHANGED
|
@@ -838,6 +838,9 @@ var Tracker = class {
|
|
|
838
838
|
this._commitStateOperation = lastOp;
|
|
839
839
|
this.reset();
|
|
840
840
|
}
|
|
841
|
+
getByTrackingId(trackingId) {
|
|
842
|
+
return this.trackedObjects.find((o) => o.trackingId === trackingId);
|
|
843
|
+
}
|
|
841
844
|
/** @internal */
|
|
842
845
|
_isInUndoStack(op) {
|
|
843
846
|
return this._undoOperations.includes(op);
|