@katn30/trakr 1.0.0 → 1.1.1
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 +171 -49
- package/dist/dev/index.cjs +8 -16
- package/dist/dev/index.cjs.map +1 -1
- package/dist/dev/index.d.cts +5 -8
- package/dist/dev/index.d.ts +5 -8
- package/dist/dev/index.js +8 -16
- package/dist/dev/index.js.map +1 -1
- package/dist/prod/index.cjs +8 -16
- package/dist/prod/index.cjs.map +1 -1
- package/dist/prod/index.js +8 -16
- package/dist/prod/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ class InvoiceModel extends TrackedObject {
|
|
|
55
55
|
|
|
56
56
|
const invoices = new TrackedCollection<InvoiceModel>(tracker);
|
|
57
57
|
const invoice = tracker.construct(() => new InvoiceModel(tracker));
|
|
58
|
-
invoices.push(invoice); // state: Insert,
|
|
58
|
+
invoices.push(invoice); // state: Insert, trackingId: 1
|
|
59
59
|
|
|
60
60
|
invoice.status = 'draft'; // recorded
|
|
61
61
|
invoice.total = 100; // recorded
|
|
@@ -489,38 +489,38 @@ Every `TrackedObject` has a `state: State` property — the single source of tru
|
|
|
489
489
|
|
|
490
490
|
#### Redo is always the same as do
|
|
491
491
|
|
|
492
|
-
There is no separate redo transition. Redo simply re-runs the original `do` action.
|
|
492
|
+
There is no separate redo transition. Redo simply re-runs the original `do` action. `trackingId` is assigned at construction and never changes, so it is always available regardless of undo/redo cycles.
|
|
493
493
|
|
|
494
494
|
#### Full transition table
|
|
495
495
|
|
|
496
|
-
| Event | Direction | From | To |
|
|
497
|
-
|
|
498
|
-
| edit | do / redo | Unchanged | **Changed** |
|
|
499
|
-
| edit | do / redo | Changed | Changed |
|
|
500
|
-
| edit | undo (last edit) | Changed | **Unchanged** |
|
|
501
|
-
| edit | undo (not last) | Changed | Changed |
|
|
502
|
-
| added | do / redo | Unchanged | **Insert** |
|
|
503
|
-
| added | undo | Insert | **Unchanged** |
|
|
504
|
-
| removed | do / redo | Insert | **Unchanged** |
|
|
505
|
-
| removed | do / redo | Unchanged | **Deleted** |
|
|
506
|
-
| removed | undo | Unchanged (was Insert) | **Insert** |
|
|
507
|
-
| removed | undo | Deleted | **Unchanged** |
|
|
508
|
-
| committed | do / redo | Insert | **Unchanged** |
|
|
509
|
-
| committed | do / redo | Changed | **Unchanged** |
|
|
510
|
-
| committed | do / redo | Deleted | **Unchanged** |
|
|
511
|
-
| committed | undo | was Insert | **Deleted** |
|
|
512
|
-
| committed | undo | was Changed | **Changed** |
|
|
513
|
-
| committed | undo | was Deleted | **Insert** |
|
|
496
|
+
| Event | Direction | From | To | `@AutoId` field |
|
|
497
|
+
|---|---|---|---|---|
|
|
498
|
+
| edit | do / redo | Unchanged | **Changed** | untouched |
|
|
499
|
+
| edit | do / redo | Changed | Changed | untouched |
|
|
500
|
+
| edit | undo (last edit) | Changed | **Unchanged** | untouched |
|
|
501
|
+
| edit | undo (not last) | Changed | Changed | untouched |
|
|
502
|
+
| added | do / redo | Unchanged | **Insert** | untouched |
|
|
503
|
+
| added | undo | Insert | **Unchanged** | untouched |
|
|
504
|
+
| removed | do / redo | Insert | **Unchanged** | untouched, dirtyCounter reset |
|
|
505
|
+
| removed | do / redo | Unchanged | **Deleted** | untouched |
|
|
506
|
+
| removed | undo | Unchanged (was Insert) | **Insert** | untouched |
|
|
507
|
+
| removed | undo | Deleted | **Unchanged** | untouched |
|
|
508
|
+
| committed | do / redo | Insert | **Unchanged** | written with real id (if key supplied) |
|
|
509
|
+
| committed | do / redo | Changed | **Unchanged** | written with real id (if key supplied) |
|
|
510
|
+
| committed | do / redo | Deleted | **Unchanged** | untouched |
|
|
511
|
+
| committed | undo | was Insert | **Deleted** | kept (real id for DELETE) |
|
|
512
|
+
| committed | undo | was Changed | **Changed** | untouched |
|
|
513
|
+
| committed | undo | was Deleted | **Insert** | kept (stale — use `trackingId` for POST) |
|
|
514
514
|
|
|
515
515
|
#### Key notes
|
|
516
516
|
|
|
517
|
-
**`removed/do` from `Insert` collapses to `Unchanged`** — if an object was added and then removed before ever being committed, it was never persisted. The transition resets `dirtyCounter` to zero
|
|
517
|
+
**`removed/do` from `Insert` collapses to `Unchanged`** — if an object was added and then removed before ever being committed, it was never persisted. The transition resets `dirtyCounter` to zero as if the add never happened. Nothing needs to be sent to the server.
|
|
518
518
|
|
|
519
519
|
**`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.
|
|
520
520
|
|
|
521
|
-
**`@AutoId` is never zeroed out** — when `committed/undo` runs after a committed INSERT, the real server id stays on the `@AutoId` field so the save layer can send `DELETE /resource/{id}`. Similarly, after `committed/undo` of a DELETE, the `@AutoId` field still holds the old real id — but since `state` is now `Insert`, the save layer must use `
|
|
521
|
+
**`@AutoId` is never zeroed out** — when `committed/undo` runs after a committed INSERT, the real server id stays on the `@AutoId` field so the save layer can send `DELETE /resource/{id}`. Similarly, after `committed/undo` of a DELETE, the `@AutoId` field still holds the old real id — but since `state` is now `Insert`, the save layer must use `trackingId` to identify the item in the POST payload, not `@AutoId`.
|
|
522
522
|
|
|
523
|
-
**`
|
|
523
|
+
**`trackingId` for `Insert` and `Changed` items** — `trackingId` is assigned at construction and never changes. Include it in the save payload for `Insert` and `Changed` items so the backend can echo back the new server-assigned PK for each. See [Recommended save pattern](#recommended-save-pattern) and [Temporally versioned tables](#temporally-versioned-tables) for usage.
|
|
524
524
|
|
|
525
525
|
### Recommended save pattern
|
|
526
526
|
|
|
@@ -528,7 +528,9 @@ trakr does not mandate a specific save strategy — you can send changes per-obj
|
|
|
528
528
|
|
|
529
529
|
That said, a pattern that works well with trakr's design is **all-or-nothing saves**: when the user clicks Save, the frontend collects every dirty object across the tracker, serialises them into a single request, and the backend saves everything inside one transaction — either succeeding fully or returning an error without applying partial changes. The frontend then calls `tracker.onCommit()` only on success.
|
|
530
530
|
|
|
531
|
-
|
|
531
|
+
Every `TrackedObject` has a `trackingId` — a positive integer assigned at construction time, stable for the lifetime of the object, unique across the tracker. Include `trackingId` in the save payload for `Insert` and `Changed` items. The backend echoes it back alongside the server-assigned PK for any item that produced a new row. `onCommit(keys)` then iterates every entry in `keys`, matches by `trackingId`, and writes the real PK to the `@AutoId` field of any match — regardless of whether the item was `Insert` or `Changed`.
|
|
532
|
+
|
|
533
|
+
New objects can reference each other via their `trackingId` in the payload (e.g. a new parent and its new children share consistent temp IDs before the server assigns real ones). After a successful save, `tracker.onCommit(keys)` updates all matched objects in place — no page reload is needed. This is the intended experience for form-heavy back-office pages, though reloading or restructuring state on save is equally valid.
|
|
532
534
|
|
|
533
535
|
**On failure, do not call `onCommit()`.**
|
|
534
536
|
|
|
@@ -577,10 +579,14 @@ Calling `undo()` or `redo()` when the respective flag is `false` is a no-op.
|
|
|
577
579
|
|
|
578
580
|
```typescript
|
|
579
581
|
tracker.onCommit(); // mark current state as committed — isDirty → false
|
|
580
|
-
tracker.onCommit(keys); // same, plus
|
|
582
|
+
tracker.onCommit(keys); // same, plus write real server IDs to @AutoId fields
|
|
581
583
|
```
|
|
582
584
|
|
|
583
|
-
`onCommit()`
|
|
585
|
+
`onCommit(keys?)` does three things:
|
|
586
|
+
|
|
587
|
+
1. Iterates every entry in `keys`. For each entry it finds a tracked object whose `trackingId` matches `entry.trackingId` and writes `entry.value` to its `@AutoId` field. This applies to both `Insert` items (new rows) and `Changed` items (e.g. temporal tables where an update produces a new row with a new PK).
|
|
588
|
+
2. Transitions every tracked object's `state` to `Unchanged` and resets `dirtyCounter`.
|
|
589
|
+
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).
|
|
584
590
|
|
|
585
591
|
**Manual composing**
|
|
586
592
|
|
|
@@ -675,7 +681,7 @@ const invoice = tracker.construct(() => new InvoiceModel(tracker));
|
|
|
675
681
|
|---|---|---|
|
|
676
682
|
| `tracker` | `Tracker` | The tracker this model belongs to (set via `super(tracker)`) |
|
|
677
683
|
| `state` | `State` | The current persistence state — `Unchanged`, `Insert`, `Changed`, or `Deleted` |
|
|
678
|
-
| `
|
|
684
|
+
| `trackingId` | `number` | Positive client-assigned identifier, unique across the tracker, set at construction and never changed. Include in the save payload for `Insert` and `Changed` items so the backend can return the new server PK |
|
|
679
685
|
| `isDirty` | `boolean` | `true` when this model has uncommitted property changes |
|
|
680
686
|
| `dirtyCounter` | `number` | Net count of uncommitted property writes. Increments on each write, decrements on undo. Reset to `0` by `onCommit()`. Can be negative after undoing past a committed save |
|
|
681
687
|
| `isValid` | `boolean` | `true` when all `@Tracked()` validators pass |
|
|
@@ -790,19 +796,20 @@ tracker.construct(() => {
|
|
|
790
796
|
new InvoiceModel(tracker, { id: 2, status: 'sent' });
|
|
791
797
|
});
|
|
792
798
|
|
|
793
|
-
// Create a new invoice and add it to a collection (state → Insert
|
|
799
|
+
// Create a new invoice and add it to a collection (state → Insert)
|
|
794
800
|
const invoices = new TrackedCollection<InvoiceModel>(tracker);
|
|
795
801
|
const newInvoice = tracker.construct(() => new InvoiceModel(tracker));
|
|
796
802
|
invoices.push(newInvoice);
|
|
797
803
|
newInvoice.status = 'pending';
|
|
798
|
-
// newInvoice.
|
|
804
|
+
// newInvoice.trackingId === 3 (assigned at construction, never changes)
|
|
805
|
+
// newInvoice.id === 0 (untouched by the library until onCommit)
|
|
799
806
|
|
|
800
807
|
// --- Save ---
|
|
801
808
|
|
|
802
809
|
// Build the payload by reading each object's state
|
|
803
810
|
const payload: {
|
|
804
|
-
inserts: {
|
|
805
|
-
updates: { id: number; status: string }[];
|
|
811
|
+
inserts: { trackingId: number; status: string }[];
|
|
812
|
+
updates: { trackingId: number; id: number; status: string }[];
|
|
806
813
|
deletes: { id: number }[];
|
|
807
814
|
} = { inserts: [], updates: [], deletes: [] };
|
|
808
815
|
|
|
@@ -810,11 +817,11 @@ for (const obj of tracker.trackedObjects) {
|
|
|
810
817
|
if (!(obj instanceof InvoiceModel)) continue;
|
|
811
818
|
switch (obj.state) {
|
|
812
819
|
case State.Insert:
|
|
813
|
-
//
|
|
814
|
-
payload.inserts.push({
|
|
820
|
+
// Send trackingId so the backend can echo back the new server PK
|
|
821
|
+
payload.inserts.push({ trackingId: obj.trackingId, status: obj.status });
|
|
815
822
|
break;
|
|
816
823
|
case State.Changed:
|
|
817
|
-
payload.updates.push({ id: obj.id, status: obj.status });
|
|
824
|
+
payload.updates.push({ trackingId: obj.trackingId, id: obj.id, status: obj.status });
|
|
818
825
|
break;
|
|
819
826
|
case State.Deleted:
|
|
820
827
|
payload.deletes.push({ id: obj.id });
|
|
@@ -826,14 +833,14 @@ for (const obj of tracker.trackedObjects) {
|
|
|
826
833
|
|
|
827
834
|
// Send to server — backend runs everything in one transaction
|
|
828
835
|
const response = await api.save(payload);
|
|
829
|
-
// response.ids: [{
|
|
836
|
+
// response.ids: [{ trackingId: 3, value: 42 }]
|
|
830
837
|
|
|
831
838
|
// Apply real IDs and mark everything clean — no page reload needed
|
|
832
839
|
tracker.onCommit(response.ids);
|
|
833
840
|
// newInvoice.id === 42, state === Unchanged
|
|
834
841
|
// tracker.isDirty === false
|
|
835
842
|
|
|
836
|
-
// When no new
|
|
843
|
+
// When no new PKs were assigned, keys can be omitted:
|
|
837
844
|
// tracker.onCommit();
|
|
838
845
|
```
|
|
839
846
|
|
|
@@ -857,33 +864,32 @@ class InvoiceModel extends TrackedObject {
|
|
|
857
864
|
}
|
|
858
865
|
```
|
|
859
866
|
|
|
860
|
-
|
|
867
|
+
The `@AutoId` field is left at its initial value until `onCommit(keys)` writes the real server ID. The save layer identifies items that need a new PK via `trackingId` — a stable, positive integer assigned at construction and never changed. Include `trackingId` in the save payload for `Insert` (and `Changed`, for temporal tables) items; the backend returns it alongside the new PK.
|
|
861
868
|
|
|
862
869
|
**Typical save flow:**
|
|
863
870
|
|
|
864
871
|
```typescript
|
|
865
872
|
const invoice = tracker.construct(() => new InvoiceModel(tracker));
|
|
866
873
|
invoices.push(invoice);
|
|
867
|
-
// invoice.
|
|
868
|
-
// invoice.id
|
|
874
|
+
// invoice.trackingId === 1 (assigned at construction, never changes)
|
|
875
|
+
// invoice.id === 0 (untouched by the library)
|
|
869
876
|
|
|
870
877
|
invoice.status = 'draft';
|
|
871
878
|
|
|
872
|
-
// 1. Build payload —
|
|
873
|
-
const serverIds = [{
|
|
879
|
+
// 1. Build payload — send trackingId for Insert items:
|
|
880
|
+
const serverIds = [{ trackingId: invoice.trackingId, value: 42 }];
|
|
874
881
|
|
|
875
882
|
// 2. Send to server, receive real IDs back.
|
|
876
883
|
|
|
877
884
|
// 3. Apply real IDs and mark clean:
|
|
878
885
|
tracker.onCommit(serverIds);
|
|
879
|
-
// invoice.id
|
|
880
|
-
//
|
|
881
|
-
// tracker.isDirty === false
|
|
886
|
+
// invoice.id === 42 (written by onCommit)
|
|
887
|
+
// tracker.isDirty === false
|
|
882
888
|
```
|
|
883
889
|
|
|
884
890
|
`onCommit()` with no arguments (or an empty array) still marks the tracker as clean — it just skips the ID replacement step.
|
|
885
891
|
|
|
886
|
-
|
|
892
|
+
`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.
|
|
887
893
|
|
|
888
894
|
---
|
|
889
895
|
|
|
@@ -915,15 +921,15 @@ The shape of each entry in the `keys` array passed to `tracker.onCommit(keys)`:
|
|
|
915
921
|
|
|
916
922
|
```typescript
|
|
917
923
|
import type { IdAssignment } from '@katn30/trakr';
|
|
918
|
-
// {
|
|
924
|
+
// { trackingId: number; value: number }
|
|
919
925
|
```
|
|
920
926
|
|
|
921
927
|
| Field | Type | Description |
|
|
922
928
|
|---|---|---|
|
|
923
|
-
| `
|
|
924
|
-
| `value` | `number` | The real server-assigned ID to
|
|
929
|
+
| `trackingId` | `number` | The `trackingId` of the object that received a new server-assigned PK |
|
|
930
|
+
| `value` | `number` | The real server-assigned ID to write to the `@AutoId` field |
|
|
925
931
|
|
|
926
|
-
The server returns one `IdAssignment` per inserted
|
|
932
|
+
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.
|
|
927
933
|
|
|
928
934
|
---
|
|
929
935
|
|
|
@@ -1203,6 +1209,122 @@ event.emit('world'); // → (nothing)
|
|
|
1203
1209
|
|
|
1204
1210
|
---
|
|
1205
1211
|
|
|
1212
|
+
## Temporally versioned tables
|
|
1213
|
+
|
|
1214
|
+
Some databases never modify or delete rows in place. Instead, each row carries a validity period — typically `dt_start_validity` and `dt_end_validity` columns. An "update" means closing the current row (`dt_end_validity = now()`) and inserting a new row with `dt_end_validity = null`. A "delete" means closing the current row the same way. This is called **Method 2 temporal versioning**.
|
|
1215
|
+
|
|
1216
|
+
Because every update produces a new database row with a new auto-increment PK, the `@AutoId` field on a `Changed` object becomes stale after a successful save: the old row it pointed to has been closed, and the new row carries a different PK. The model must be updated with the new PK before the next save, otherwise the save layer would try to close the wrong row.
|
|
1217
|
+
|
|
1218
|
+
trakr handles this through `trackingId` and `onCommit`. The save flow for temporal tables is the same as the standard flow — the only difference is that the backend also returns `{ trackingId, value }` entries for `Changed` items (not just `Insert` items), and `onCommit(keys)` writes the new PK to those objects too.
|
|
1219
|
+
|
|
1220
|
+
### The problem
|
|
1221
|
+
|
|
1222
|
+
In a standard (non-temporal) database, an UPDATE modifies a row in place. The PK stays the same. After commit, `obj.id` is still correct.
|
|
1223
|
+
|
|
1224
|
+
In a temporal database, an UPDATE closes the current row and inserts a new one. The new row has a fresh PK. After commit, `obj.id` points to the closed row — it is now stale.
|
|
1225
|
+
|
|
1226
|
+
```
|
|
1227
|
+
Before save: obj.id = 10 (current, open row)
|
|
1228
|
+
Backend: closes row 10, inserts row 99
|
|
1229
|
+
After save: obj.id = 10 (stale — row 10 is closed)
|
|
1230
|
+
Next save: tries to close row 10 → wrong row
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
### The solution
|
|
1234
|
+
|
|
1235
|
+
Include `trackingId` in the payload for `Changed` items. The backend returns `{ trackingId, value }` for every item that produced a new row — inserts and temporal updates alike. `onCommit(keys)` writes the new PK to the `@AutoId` field of every matched object.
|
|
1236
|
+
|
|
1237
|
+
```
|
|
1238
|
+
Before save: obj.trackingId = 3, obj.id = 10
|
|
1239
|
+
Payload: { trackingId: 3, id: 10, ...fields }
|
|
1240
|
+
Backend: closes row 10, inserts row 99, echoes { trackingId: 3, value: 99 }
|
|
1241
|
+
onCommit: writes 99 to obj.id
|
|
1242
|
+
After save: obj.id = 99 (correct, open row), state = Unchanged
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
### Full example
|
|
1246
|
+
|
|
1247
|
+
```typescript
|
|
1248
|
+
import { Tracker, TrackedObject, TrackedCollection, State, Tracked, AutoId } from '@katn30/trakr';
|
|
1249
|
+
|
|
1250
|
+
class RuleModel extends TrackedObject {
|
|
1251
|
+
@AutoId
|
|
1252
|
+
id: number = 0;
|
|
1253
|
+
|
|
1254
|
+
@Tracked()
|
|
1255
|
+
accessor value: string = '';
|
|
1256
|
+
|
|
1257
|
+
constructor(tracker: Tracker) {
|
|
1258
|
+
super(tracker);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const tracker = new Tracker();
|
|
1263
|
+
|
|
1264
|
+
// Load existing rows from the server
|
|
1265
|
+
const rule = tracker.construct(() => new RuleModel(tracker));
|
|
1266
|
+
tracker.withTrackingSuppressed(() => { rule.id = 10; });
|
|
1267
|
+
// rule.state === Unchanged
|
|
1268
|
+
// rule.trackingId === 1 (assigned at construction)
|
|
1269
|
+
// rule.id === 10 (real server PK)
|
|
1270
|
+
|
|
1271
|
+
// User edits a value
|
|
1272
|
+
rule.value = '24h';
|
|
1273
|
+
// rule.state === Changed
|
|
1274
|
+
|
|
1275
|
+
// --- Save ---
|
|
1276
|
+
|
|
1277
|
+
const payload = {
|
|
1278
|
+
inserts: [] as { trackingId: number; value: string }[],
|
|
1279
|
+
changes: [] as { trackingId: number; id: number; value: string }[],
|
|
1280
|
+
deletes: [] as { id: number }[],
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
for (const obj of tracker.trackedObjects) {
|
|
1284
|
+
if (!(obj instanceof RuleModel)) continue;
|
|
1285
|
+
switch (obj.state) {
|
|
1286
|
+
case State.Insert:
|
|
1287
|
+
payload.inserts.push({ trackingId: obj.trackingId, value: obj.value });
|
|
1288
|
+
break;
|
|
1289
|
+
case State.Changed:
|
|
1290
|
+
// Send both trackingId (to correlate the response) and id (to close the right row)
|
|
1291
|
+
payload.changes.push({ trackingId: obj.trackingId, id: obj.id, value: obj.value });
|
|
1292
|
+
break;
|
|
1293
|
+
case State.Deleted:
|
|
1294
|
+
payload.deletes.push({ id: obj.id });
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Backend closes row 10, inserts row 99, returns the mapping
|
|
1300
|
+
const response = await api.save(payload);
|
|
1301
|
+
// response.ids: [{ trackingId: 1, value: 99 }] ← returned for both inserts and temporal changes
|
|
1302
|
+
|
|
1303
|
+
// onCommit writes 99 to rule.id, transitions state to Unchanged
|
|
1304
|
+
tracker.onCommit(response.ids);
|
|
1305
|
+
// rule.id === 99 (new open row)
|
|
1306
|
+
// rule.state === Unchanged
|
|
1307
|
+
// tracker.isDirty === false
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
### Undo after a temporal commit
|
|
1311
|
+
|
|
1312
|
+
If the user undoes past a committed temporal update, the object transitions back to `Changed` with the old field values restored by the property undo closures. On the next save, `obj.id` now holds `99` (the last committed PK), which is correct — the backend can use it to close row 99 and open a new one.
|
|
1313
|
+
|
|
1314
|
+
```
|
|
1315
|
+
onCommit: rule.id = 99, state = Unchanged
|
|
1316
|
+
tracker.undo() rule.value restored to previous value, state = Changed
|
|
1317
|
+
Next save: payload.changes includes { trackingId: 1, id: 99, value: '...' }
|
|
1318
|
+
Backend: closes row 99, inserts row 100, returns { trackingId: 1, value: 100 }
|
|
1319
|
+
onCommit: rule.id = 100
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
### Deleted items
|
|
1323
|
+
|
|
1324
|
+
For `Deleted` items the PK never changes — the backend just closes the existing row. No `trackingId` is needed in the delete payload; `obj.id` is always the correct row to close.
|
|
1325
|
+
|
|
1326
|
+
---
|
|
1327
|
+
|
|
1206
1328
|
## License
|
|
1207
1329
|
|
|
1208
1330
|
MIT — Nazario Mazzotti
|
package/dist/dev/index.cjs
CHANGED
|
@@ -253,7 +253,7 @@ function validateSingleProperty(tracked, property) {
|
|
|
253
253
|
var Tracker = class {
|
|
254
254
|
constructor() {
|
|
255
255
|
this._suppressTrackingCounter = 0;
|
|
256
|
-
this.
|
|
256
|
+
this._trackingIdCounter = 1;
|
|
257
257
|
this._invalidCount = 0;
|
|
258
258
|
this._constructionDepth = 0;
|
|
259
259
|
this._version = 0;
|
|
@@ -405,6 +405,7 @@ var Tracker = class {
|
|
|
405
405
|
this._currentOperationPropertyName = properties.property;
|
|
406
406
|
if (this.shouldCoalesceChanges(properties)) {
|
|
407
407
|
this._currentOperation = CollectionUtilities.getLast(this._undoOperations);
|
|
408
|
+
this._version++;
|
|
408
409
|
this.versionChanged.emit(this._version);
|
|
409
410
|
} else {
|
|
410
411
|
this._currentOperation = new Operation();
|
|
@@ -465,8 +466,8 @@ var Tracker = class {
|
|
|
465
466
|
return (/* @__PURE__ */ new Date()).getTime() - CollectionUtilities.getLast(lastOperation.actions).time.getTime() < coalesceWithin;
|
|
466
467
|
}
|
|
467
468
|
/** @internal */
|
|
468
|
-
|
|
469
|
-
return this.
|
|
469
|
+
_nextTrackingId() {
|
|
470
|
+
return this._trackingIdCounter++;
|
|
470
471
|
}
|
|
471
472
|
onCommit(keys) {
|
|
472
473
|
const lastOp = CollectionUtilities.getLast(this._undoOperations);
|
|
@@ -603,10 +604,8 @@ function applyStateTransition(obj, event, direction, context) {
|
|
|
603
604
|
function applyAdded(obj, direction) {
|
|
604
605
|
if (direction === "undo") {
|
|
605
606
|
obj._setState("Unchanged" /* Unchanged */);
|
|
606
|
-
obj.idPlaceholder = null;
|
|
607
607
|
} else {
|
|
608
608
|
obj._setState("Insert" /* Insert */);
|
|
609
|
-
obj.idPlaceholder = obj.tracker._nextPlaceholder();
|
|
610
609
|
}
|
|
611
610
|
}
|
|
612
611
|
function applyRemoved(obj, direction, context) {
|
|
@@ -614,17 +613,13 @@ function applyRemoved(obj, direction, context) {
|
|
|
614
613
|
const prev = context?.prevState ?? "Unchanged" /* Unchanged */;
|
|
615
614
|
obj._setState(prev);
|
|
616
615
|
if (prev === "Insert" /* Insert */) {
|
|
617
|
-
obj.idPlaceholder = obj.tracker._nextPlaceholder();
|
|
618
616
|
if (context?.prevDirtyCounter !== void 0) {
|
|
619
617
|
obj._setDirtyCounter(context.prevDirtyCounter);
|
|
620
618
|
}
|
|
621
|
-
} else {
|
|
622
|
-
obj.idPlaceholder = null;
|
|
623
619
|
}
|
|
624
620
|
} else {
|
|
625
621
|
if (obj.state === "Insert" /* Insert */) {
|
|
626
622
|
obj._setState("Unchanged" /* Unchanged */);
|
|
627
|
-
obj.idPlaceholder = null;
|
|
628
623
|
obj._setDirtyCounter(0);
|
|
629
624
|
} else {
|
|
630
625
|
obj._setState("Deleted" /* Deleted */);
|
|
@@ -636,18 +631,15 @@ function applyCommitted(obj, direction, context) {
|
|
|
636
631
|
const prev = context?.prevState ?? "Unchanged" /* Unchanged */;
|
|
637
632
|
if (prev === "Insert" /* Insert */) {
|
|
638
633
|
obj._setState("Deleted" /* Deleted */);
|
|
639
|
-
obj.idPlaceholder = null;
|
|
640
634
|
} else if (prev === "Deleted" /* Deleted */) {
|
|
641
635
|
obj._setState("Insert" /* Insert */);
|
|
642
|
-
obj.idPlaceholder = obj.tracker._nextPlaceholder();
|
|
643
636
|
} else if (prev === "Changed" /* Changed */) {
|
|
644
637
|
obj._setState("Changed" /* Changed */);
|
|
645
638
|
}
|
|
646
639
|
} else {
|
|
647
|
-
if (context?.prevState === "Insert" /* Insert */ && context.autoIdProp && context.realId !== void 0) {
|
|
640
|
+
if ((context?.prevState === "Insert" /* Insert */ || context?.prevState === "Changed" /* Changed */) && context.autoIdProp && context.realId !== void 0) {
|
|
648
641
|
obj[context.autoIdProp] = context.realId;
|
|
649
642
|
}
|
|
650
|
-
obj.idPlaceholder = null;
|
|
651
643
|
obj._setState("Unchanged" /* Unchanged */);
|
|
652
644
|
obj._setDirtyCounter(0);
|
|
653
645
|
}
|
|
@@ -655,8 +647,8 @@ function applyCommitted(obj, direction, context) {
|
|
|
655
647
|
function buildCommittedContext(obj, autoIdProp, keys) {
|
|
656
648
|
const prevState = obj.state;
|
|
657
649
|
let realId;
|
|
658
|
-
if (prevState === "Insert" /* Insert */
|
|
659
|
-
realId = keys.find((k) => k.
|
|
650
|
+
if ((prevState === "Insert" /* Insert */ || prevState === "Changed" /* Changed */) && keys) {
|
|
651
|
+
realId = keys.find((k) => k.trackingId === obj.trackingId)?.value;
|
|
660
652
|
}
|
|
661
653
|
return { prevState, autoIdProp, realId };
|
|
662
654
|
}
|
|
@@ -668,12 +660,12 @@ var TrackedObject = class {
|
|
|
668
660
|
this._dirtyCounter = 0;
|
|
669
661
|
this._isValid = true;
|
|
670
662
|
this._state = "Unchanged" /* Unchanged */;
|
|
671
|
-
this.idPlaceholder = null;
|
|
672
663
|
this.changed = new TypedEvent();
|
|
673
664
|
this.trackedChanged = new TypedEvent();
|
|
674
665
|
if (!tracker._isConstructing) {
|
|
675
666
|
throw new Error(`${this.constructor.name} must be created inside tracker.construct()`);
|
|
676
667
|
}
|
|
668
|
+
this.trackingId = tracker._nextTrackingId();
|
|
677
669
|
this.validationMessages = /* @__PURE__ */ new Map();
|
|
678
670
|
tracker._trackObject(this);
|
|
679
671
|
}
|