@abloatai/humans 0.66.1 → 0.66.2
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.
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* counterparts (`paired`), which carry the value each operation established: on
|
|
17
17
|
* undo the forwards say what you set, and on redo the inverses say what undo
|
|
18
18
|
* restored. For `update` and `updateMany` operations it drops any field whose
|
|
19
|
-
* live value no longer matches that established value.
|
|
19
|
+
* live value no longer matches that established value. Plain JSON objects are
|
|
20
|
+
* compared recursively, so independent nested changes can still be undone.
|
|
21
|
+
* Arrays and object/type replacements remain atomic. The `create` and
|
|
20
22
|
* `delete` families are structural and always applied — undoing a create
|
|
21
23
|
* removes the row you added, and undoing a delete restores it.
|
|
22
24
|
*
|
|
@@ -30,7 +32,7 @@ import { deepEqual } from '@abloatai/transaction/utils/json';
|
|
|
30
32
|
* How undo and redo treat a field that a collaborator changed after your
|
|
31
33
|
* operation.
|
|
32
34
|
*
|
|
33
|
-
* - `skip-stale` (the default): leave
|
|
35
|
+
* - `skip-stale` (the default): leave superseded fields/JSON leaves alone. Your change has
|
|
34
36
|
* already been superseded, so reverting it would overwrite the
|
|
35
37
|
* collaborator's value. This is what keeps undo scoped to your own edits.
|
|
36
38
|
* - `last-writer-wins`: apply the operation verbatim, so your undo overwrites
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* counterparts (`paired`), which carry the value each operation established: on
|
|
17
17
|
* undo the forwards say what you set, and on redo the inverses say what undo
|
|
18
18
|
* restored. For `update` and `updateMany` operations it drops any field whose
|
|
19
|
-
* live value no longer matches that established value.
|
|
19
|
+
* live value no longer matches that established value. Plain JSON objects are
|
|
20
|
+
* compared recursively, so independent nested changes can still be undone.
|
|
21
|
+
* Arrays and object/type replacements remain atomic. The `create` and
|
|
20
22
|
* `delete` families are structural and always applied — undoing a create
|
|
21
23
|
* removes the row you added, and undoing a delete restores it.
|
|
22
24
|
*
|
|
@@ -56,10 +58,34 @@ function readCurrentField(store, id, field) {
|
|
|
56
58
|
const json = model.toJSON?.();
|
|
57
59
|
return json ? json[field] : undefined;
|
|
58
60
|
}
|
|
61
|
+
// Missing object properties differ from properties whose value is undefined.
|
|
62
|
+
const MISSING = Symbol('missing undo property');
|
|
63
|
+
const ownValue = (value, key) => Object.hasOwn(value, key) ? value[key] : MISSING;
|
|
64
|
+
function isPlainObject(value) {
|
|
65
|
+
if (value === null || typeof value !== 'object')
|
|
66
|
+
return false;
|
|
67
|
+
const prototype = Object.getPrototypeOf(value);
|
|
68
|
+
return prototype === Object.prototype || prototype === null;
|
|
69
|
+
}
|
|
70
|
+
/** Replay changed JSON leaves; arrays and replacements remain atomic. */
|
|
71
|
+
function replayValue(value, established, current) {
|
|
72
|
+
if (deepEqual(value, established))
|
|
73
|
+
return current;
|
|
74
|
+
if (deepEqual(current, established))
|
|
75
|
+
return value;
|
|
76
|
+
if (isPlainObject(value) && isPlainObject(established) && isPlainObject(current)) {
|
|
77
|
+
const keys = new Set([...Object.keys(current), ...Object.keys(established), ...Object.keys(value)]);
|
|
78
|
+
// fromEntries keeps keys such as __proto__ as ordinary own properties.
|
|
79
|
+
return Object.fromEntries([...keys].flatMap(key => {
|
|
80
|
+
const next = replayValue(ownValue(value, key), ownValue(established, key), ownValue(current, key));
|
|
81
|
+
return next === MISSING ? [] : [[key, next]];
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
59
86
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* no-op — every field was superseded by a collaborator).
|
|
87
|
+
* Replay only changes that still stand, retaining collaborators' JSON leaves.
|
|
88
|
+
* Returns `null` when no effective change survives.
|
|
63
89
|
*/
|
|
64
90
|
function filterStalePatch(store, patch, established) {
|
|
65
91
|
const out = { id: patch.id };
|
|
@@ -68,11 +94,10 @@ function filterStalePatch(store, patch, established) {
|
|
|
68
94
|
if (field === 'id')
|
|
69
95
|
continue;
|
|
70
96
|
if (established && field in established) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
out[field] = patch[field];
|
|
97
|
+
const current = readCurrentField(store, patch.id, field);
|
|
98
|
+
const next = replayValue(patch[field], established[field], current);
|
|
99
|
+
if (!deepEqual(next, current)) {
|
|
100
|
+
out[field] = next;
|
|
76
101
|
kept++;
|
|
77
102
|
}
|
|
78
103
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/humans",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.2",
|
|
4
4
|
"description": "The optional human-facing local-state package for Ablo: presence, live queries, and React bindings.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"directory": "packages/humans"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"@abloatai/transaction": "0.66.
|
|
88
|
+
"@abloatai/transaction": "0.66.2",
|
|
89
89
|
"events": "^3.3.0",
|
|
90
90
|
"mobx": "^6.13.7",
|
|
91
91
|
"uuid": "^11.1.0",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* counterparts (`paired`), which carry the value each operation established: on
|
|
17
17
|
* undo the forwards say what you set, and on redo the inverses say what undo
|
|
18
18
|
* restored. For `update` and `updateMany` operations it drops any field whose
|
|
19
|
-
* live value no longer matches that established value.
|
|
19
|
+
* live value no longer matches that established value. Plain JSON objects are
|
|
20
|
+
* compared recursively, so independent nested changes can still be undone.
|
|
21
|
+
* Arrays and object/type replacements remain atomic. The `create` and
|
|
20
22
|
* `delete` families are structural and always applied — undoing a create
|
|
21
23
|
* removes the row you added, and undoing a delete restores it.
|
|
22
24
|
*
|
|
@@ -32,7 +34,7 @@ import { deepEqual } from '@abloatai/transaction/utils/json';
|
|
|
32
34
|
* How undo and redo treat a field that a collaborator changed after your
|
|
33
35
|
* operation.
|
|
34
36
|
*
|
|
35
|
-
* - `skip-stale` (the default): leave
|
|
37
|
+
* - `skip-stale` (the default): leave superseded fields/JSON leaves alone. Your change has
|
|
36
38
|
* already been superseded, so reverting it would overwrite the
|
|
37
39
|
* collaborator's value. This is what keeps undo scoped to your own edits.
|
|
38
40
|
* - `last-writer-wins`: apply the operation verbatim, so your undo overwrites
|
|
@@ -75,10 +77,35 @@ function readCurrentField(store: SyncStoreContract, id: string, field: string):
|
|
|
75
77
|
|
|
76
78
|
type Patch = { id: string } & Record<string, unknown>;
|
|
77
79
|
|
|
80
|
+
// Missing object properties differ from properties whose value is undefined.
|
|
81
|
+
const MISSING = Symbol('missing undo property');
|
|
82
|
+
const ownValue = (value: Record<string, unknown>, key: string): unknown =>
|
|
83
|
+
Object.hasOwn(value, key) ? value[key] : MISSING;
|
|
84
|
+
|
|
85
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
86
|
+
if (value === null || typeof value !== 'object') return false;
|
|
87
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
88
|
+
return prototype === Object.prototype || prototype === null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Replay changed JSON leaves; arrays and replacements remain atomic. */
|
|
92
|
+
function replayValue(value: unknown, established: unknown, current: unknown): unknown {
|
|
93
|
+
if (deepEqual(value, established)) return current;
|
|
94
|
+
if (deepEqual(current, established)) return value;
|
|
95
|
+
if (isPlainObject(value) && isPlainObject(established) && isPlainObject(current)) {
|
|
96
|
+
const keys = new Set([...Object.keys(current), ...Object.keys(established), ...Object.keys(value)]);
|
|
97
|
+
// fromEntries keeps keys such as __proto__ as ordinary own properties.
|
|
98
|
+
return Object.fromEntries([...keys].flatMap(key => {
|
|
99
|
+
const next = replayValue(ownValue(value, key), ownValue(established, key), ownValue(current, key));
|
|
100
|
+
return next === MISSING ? [] : [[key, next]];
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
return current;
|
|
104
|
+
}
|
|
105
|
+
|
|
78
106
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* no-op — every field was superseded by a collaborator).
|
|
107
|
+
* Replay only changes that still stand, retaining collaborators' JSON leaves.
|
|
108
|
+
* Returns `null` when no effective change survives.
|
|
82
109
|
*/
|
|
83
110
|
function filterStalePatch(
|
|
84
111
|
store: SyncStoreContract,
|
|
@@ -90,11 +117,10 @@ function filterStalePatch(
|
|
|
90
117
|
for (const field of Object.keys(patch)) {
|
|
91
118
|
if (field === 'id') continue;
|
|
92
119
|
if (established && field in established) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
out[field] = patch[field];
|
|
120
|
+
const current = readCurrentField(store, patch.id, field);
|
|
121
|
+
const next = replayValue(patch[field], established[field], current);
|
|
122
|
+
if (!deepEqual(next, current)) {
|
|
123
|
+
out[field] = next;
|
|
98
124
|
kept++;
|
|
99
125
|
}
|
|
100
126
|
} else {
|