@buerli.io/core 1.1.0-beta.8 → 1.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/build/cjs/store/states/immutablePatch.js +166 -0
- package/build/cjs/store/states/structure.js +17 -15
- package/build/esm/store/states/immutablePatch.js +162 -0
- package/build/esm/store/states/structure.js +17 -12
- package/build/types/store/states/immutablePatch.d.ts +30 -0
- package/build/types/store/states/structure.d.ts +7 -1
- package/package.json +2 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* applyPatchImmutable — RFC 6902 (JSON Patch) applied with structural sharing.
|
|
4
|
+
*
|
|
5
|
+
* Never mutates the input document. Copies exactly the containers on the path
|
|
6
|
+
* of each operation (copy-on-write); untouched siblings keep their identity,
|
|
7
|
+
* so store subscribers with reference-equality selectors only wake where
|
|
8
|
+
* something actually changed.
|
|
9
|
+
*
|
|
10
|
+
* The speed trick: a per-batch `cloned` set tracks containers that are already
|
|
11
|
+
* fresh copies owned by this batch — those are mutated in place. A batch of
|
|
12
|
+
* N ops therefore costs one shallow copy per DISTINCT touched container (plus
|
|
13
|
+
* the walk), not one copy chain per op. No full deep clone, ever.
|
|
14
|
+
*
|
|
15
|
+
* This replaces the previous two-library setup (jsonpatch for small blocks,
|
|
16
|
+
* fast-json-patch with a full deep clone for blocks > 100 ops): it is faster
|
|
17
|
+
* than either in its own domain AND keeps identities correct, so the 100-op
|
|
18
|
+
* threshold and its discontinuous identity behavior are gone.
|
|
19
|
+
*
|
|
20
|
+
* Supported ops: add, remove, replace, move, copy, test (the full RFC set;
|
|
21
|
+
* the engine's json::diff emits only add/remove/replace). Throws on invalid
|
|
22
|
+
* paths/ops — the caller decides how to recover, exactly like the previous
|
|
23
|
+
* jsonpatch behavior.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.applyPatchImmutable = void 0;
|
|
27
|
+
/** '/a/b~1c/0' → ['a', 'b/c', '0'] with RFC 6901 unescaping (~1 → '/', ~0 → '~'). */
|
|
28
|
+
const parsePointer = (path) => {
|
|
29
|
+
if (path === '')
|
|
30
|
+
return [];
|
|
31
|
+
if (path[0] !== '/')
|
|
32
|
+
throw new Error(`invalid JSON pointer: ${path}`);
|
|
33
|
+
return path
|
|
34
|
+
.slice(1)
|
|
35
|
+
.split('/')
|
|
36
|
+
.map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
37
|
+
};
|
|
38
|
+
const toIndex = (seg, arr, allowEnd) => {
|
|
39
|
+
const idx = seg === '-' ? arr.length : Number(seg);
|
|
40
|
+
if (!Number.isInteger(idx) || idx < 0 || idx > arr.length - (allowEnd ? 0 : 1)) {
|
|
41
|
+
throw new Error(`array index out of bounds: ${seg}`);
|
|
42
|
+
}
|
|
43
|
+
return idx;
|
|
44
|
+
};
|
|
45
|
+
const deepEqual = (a, b) => {
|
|
46
|
+
if (a === b)
|
|
47
|
+
return true;
|
|
48
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
49
|
+
return false;
|
|
50
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
51
|
+
return false;
|
|
52
|
+
const ka = Object.keys(a);
|
|
53
|
+
if (ka.length !== Object.keys(b).length)
|
|
54
|
+
return false;
|
|
55
|
+
return ka.every(k => deepEqual(a[k], b[k]));
|
|
56
|
+
};
|
|
57
|
+
const getAtPath = (doc, segs) => {
|
|
58
|
+
let cur = doc;
|
|
59
|
+
for (const seg of segs) {
|
|
60
|
+
if (cur === null || typeof cur !== 'object')
|
|
61
|
+
throw new Error('path not found');
|
|
62
|
+
cur = Array.isArray(cur) ? cur[toIndex(seg, cur, false)] : cur[seg];
|
|
63
|
+
if (cur === undefined)
|
|
64
|
+
throw new Error('path not found');
|
|
65
|
+
}
|
|
66
|
+
return cur;
|
|
67
|
+
};
|
|
68
|
+
/** Portable deep clone for the rare `copy` op (structuredClone needs node >= 17). */
|
|
69
|
+
const deepClone = (v) => typeof structuredClone === 'function' ? structuredClone(v) : JSON.parse(JSON.stringify(v));
|
|
70
|
+
function applyPatchImmutable(doc, ops) {
|
|
71
|
+
// Containers created by THIS batch — safe to mutate in place. Keyed by the
|
|
72
|
+
// copies themselves, so shared history from the input doc is never touched.
|
|
73
|
+
const cloned = new Set();
|
|
74
|
+
/** Return a batch-owned version of `container` (copying it if needed). */
|
|
75
|
+
const own = (container) => {
|
|
76
|
+
if (cloned.has(container))
|
|
77
|
+
return container;
|
|
78
|
+
const copy = (Array.isArray(container) ? container.slice() : Object.assign({}, container));
|
|
79
|
+
cloned.add(copy);
|
|
80
|
+
return copy;
|
|
81
|
+
};
|
|
82
|
+
let root = doc;
|
|
83
|
+
const applyOne = (op) => {
|
|
84
|
+
var _a, _b, _c;
|
|
85
|
+
const segs = parsePointer(op.path);
|
|
86
|
+
// Whole-document operations.
|
|
87
|
+
if (segs.length === 0) {
|
|
88
|
+
switch (op.op) {
|
|
89
|
+
case 'replace':
|
|
90
|
+
case 'add':
|
|
91
|
+
root = op.value;
|
|
92
|
+
return;
|
|
93
|
+
case 'test':
|
|
94
|
+
if (!deepEqual(root, op.value))
|
|
95
|
+
throw new Error('test failed at root');
|
|
96
|
+
return;
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`op "${op.op}" not allowed on document root`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// move/copy decompose into get + remove/add. Reusing the moved reference
|
|
102
|
+
// is safe (it ends up at exactly one location); copy inserts a detached
|
|
103
|
+
// clone so a later op on one copy can never alias into the other.
|
|
104
|
+
if (op.op === 'move') {
|
|
105
|
+
const value = getAtPath(root, parsePointer((_a = op.from) !== null && _a !== void 0 ? _a : ''));
|
|
106
|
+
applyOne({ op: 'remove', path: (_b = op.from) !== null && _b !== void 0 ? _b : '' });
|
|
107
|
+
applyOne({ op: 'add', path: op.path, value });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (op.op === 'copy') {
|
|
111
|
+
applyOne({ op: 'add', path: op.path, value: deepClone(getAtPath(root, parsePointer((_c = op.from) !== null && _c !== void 0 ? _c : ''))) });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Copy-on-write walk: own every container from the root down to the
|
|
115
|
+
// operation's parent, re-linking each owned copy into its (owned) parent.
|
|
116
|
+
root = own(root);
|
|
117
|
+
let parent = root;
|
|
118
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
119
|
+
const key = Array.isArray(parent) ? toIndex(segs[i], parent, false) : segs[i];
|
|
120
|
+
const child = parent[key];
|
|
121
|
+
if (child === null || typeof child !== 'object')
|
|
122
|
+
throw new Error(`path not found: ${op.path}`);
|
|
123
|
+
const owned = own(child);
|
|
124
|
+
if (owned !== child)
|
|
125
|
+
parent[key] = owned; // parent is owned → safe
|
|
126
|
+
parent = owned;
|
|
127
|
+
}
|
|
128
|
+
const last = segs[segs.length - 1];
|
|
129
|
+
switch (op.op) {
|
|
130
|
+
case 'replace':
|
|
131
|
+
if (Array.isArray(parent))
|
|
132
|
+
parent[toIndex(last, parent, false)] = op.value;
|
|
133
|
+
else if (last in parent)
|
|
134
|
+
parent[last] = op.value;
|
|
135
|
+
else
|
|
136
|
+
throw new Error(`replace target missing: ${op.path}`);
|
|
137
|
+
return;
|
|
138
|
+
case 'add':
|
|
139
|
+
if (Array.isArray(parent))
|
|
140
|
+
parent.splice(toIndex(last, parent, true), 0, op.value);
|
|
141
|
+
else
|
|
142
|
+
parent[last] = op.value;
|
|
143
|
+
return;
|
|
144
|
+
case 'remove':
|
|
145
|
+
if (Array.isArray(parent))
|
|
146
|
+
parent.splice(toIndex(last, parent, false), 1);
|
|
147
|
+
else if (last in parent)
|
|
148
|
+
delete parent[last];
|
|
149
|
+
else
|
|
150
|
+
throw new Error(`remove target missing: ${op.path}`);
|
|
151
|
+
return;
|
|
152
|
+
case 'test': {
|
|
153
|
+
const actual = Array.isArray(parent) ? parent[toIndex(last, parent, false)] : parent[last];
|
|
154
|
+
if (!deepEqual(actual, op.value))
|
|
155
|
+
throw new Error(`test failed: ${op.path}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
throw new Error(`unknown op: ${op.op}`);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
for (const op of ops)
|
|
163
|
+
applyOne(op);
|
|
164
|
+
return root;
|
|
165
|
+
}
|
|
166
|
+
exports.applyPatchImmutable = applyPatchImmutable;
|
|
@@ -22,16 +22,12 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
22
22
|
__setModuleDefault(result, mod);
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
-
};
|
|
28
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
26
|
exports.structureApi = exports.structureData = void 0;
|
|
30
|
-
const fast_json_patch_1 = __importDefault(require("fast-json-patch"));
|
|
31
|
-
const jsonpatch_1 = __importDefault(require("jsonpatch"));
|
|
32
27
|
const THREE = __importStar(require("three"));
|
|
33
28
|
const types_1 = require("../../types");
|
|
34
29
|
const Math_1 = require("../../utils/Math");
|
|
30
|
+
const immutablePatch_1 = require("./immutablePatch");
|
|
35
31
|
////////////////////////////////////////////////////
|
|
36
32
|
// DATA
|
|
37
33
|
////////////////////////////////////////////////////
|
|
@@ -58,28 +54,33 @@ const structureApi = (set, get, buerliApi) => ({
|
|
|
58
54
|
// console.log('cleanup')
|
|
59
55
|
api.structure.postProcessStructureUpdate();
|
|
60
56
|
},
|
|
57
|
+
/**
|
|
58
|
+
* Applies structure patches to the tree. Returns `false` when a patch block
|
|
59
|
+
* failed to apply — the tree is then diverged from the server's baseline and
|
|
60
|
+
* stays diverged (patches are relative), so the caller should recover by
|
|
61
|
+
* re-fetching the full tree.
|
|
62
|
+
*/
|
|
61
63
|
applyPatches: (patches) => {
|
|
62
64
|
const before = get().structure.products.concat(get().structure.visibleContainers);
|
|
65
|
+
let ok = true;
|
|
63
66
|
const applyBlock = (block) => {
|
|
64
67
|
if (block.length === 0)
|
|
65
68
|
return;
|
|
66
69
|
set(state => {
|
|
67
70
|
let structure = state.structure;
|
|
68
71
|
try {
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
structure = jsonpatch_1.default.apply_patch(state.structure, block);
|
|
77
|
-
}
|
|
72
|
+
// Structural-sharing patch application: only containers on a patched
|
|
73
|
+
// path get new identities, untouched subtrees stay reference-equal —
|
|
74
|
+
// so identity-sensitive subscribers/effects only react to genuine
|
|
75
|
+
// changes. Replaces the former jsonpatch/fast-json-patch split (the
|
|
76
|
+
// > 100 ops fast-json-patch path deep-cloned the WHOLE document and
|
|
77
|
+
// gave every node a new identity on every large block).
|
|
78
|
+
structure = (0, immutablePatch_1.applyPatchImmutable)(state.structure, block);
|
|
78
79
|
}
|
|
79
80
|
catch (e) {
|
|
81
|
+
ok = false;
|
|
80
82
|
console.error(e);
|
|
81
83
|
}
|
|
82
|
-
// console.info('currentProduct in new patched structure: ', structure.currentProduct)
|
|
83
84
|
return { structure: Object.assign(Object.assign({}, state.structure), structure) };
|
|
84
85
|
});
|
|
85
86
|
};
|
|
@@ -111,6 +112,7 @@ const structureApi = (set, get, buerliApi) => ({
|
|
|
111
112
|
if (before.sort().join() !== after.sort().join()) {
|
|
112
113
|
api.geometry.updateBounds();
|
|
113
114
|
}
|
|
115
|
+
return ok;
|
|
114
116
|
},
|
|
115
117
|
filterTree: (tree, filter) => {
|
|
116
118
|
const objs = [];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* applyPatchImmutable — RFC 6902 (JSON Patch) applied with structural sharing.
|
|
3
|
+
*
|
|
4
|
+
* Never mutates the input document. Copies exactly the containers on the path
|
|
5
|
+
* of each operation (copy-on-write); untouched siblings keep their identity,
|
|
6
|
+
* so store subscribers with reference-equality selectors only wake where
|
|
7
|
+
* something actually changed.
|
|
8
|
+
*
|
|
9
|
+
* The speed trick: a per-batch `cloned` set tracks containers that are already
|
|
10
|
+
* fresh copies owned by this batch — those are mutated in place. A batch of
|
|
11
|
+
* N ops therefore costs one shallow copy per DISTINCT touched container (plus
|
|
12
|
+
* the walk), not one copy chain per op. No full deep clone, ever.
|
|
13
|
+
*
|
|
14
|
+
* This replaces the previous two-library setup (jsonpatch for small blocks,
|
|
15
|
+
* fast-json-patch with a full deep clone for blocks > 100 ops): it is faster
|
|
16
|
+
* than either in its own domain AND keeps identities correct, so the 100-op
|
|
17
|
+
* threshold and its discontinuous identity behavior are gone.
|
|
18
|
+
*
|
|
19
|
+
* Supported ops: add, remove, replace, move, copy, test (the full RFC set;
|
|
20
|
+
* the engine's json::diff emits only add/remove/replace). Throws on invalid
|
|
21
|
+
* paths/ops — the caller decides how to recover, exactly like the previous
|
|
22
|
+
* jsonpatch behavior.
|
|
23
|
+
*/
|
|
24
|
+
/** '/a/b~1c/0' → ['a', 'b/c', '0'] with RFC 6901 unescaping (~1 → '/', ~0 → '~'). */
|
|
25
|
+
const parsePointer = (path) => {
|
|
26
|
+
if (path === '')
|
|
27
|
+
return [];
|
|
28
|
+
if (path[0] !== '/')
|
|
29
|
+
throw new Error(`invalid JSON pointer: ${path}`);
|
|
30
|
+
return path
|
|
31
|
+
.slice(1)
|
|
32
|
+
.split('/')
|
|
33
|
+
.map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
34
|
+
};
|
|
35
|
+
const toIndex = (seg, arr, allowEnd) => {
|
|
36
|
+
const idx = seg === '-' ? arr.length : Number(seg);
|
|
37
|
+
if (!Number.isInteger(idx) || idx < 0 || idx > arr.length - (allowEnd ? 0 : 1)) {
|
|
38
|
+
throw new Error(`array index out of bounds: ${seg}`);
|
|
39
|
+
}
|
|
40
|
+
return idx;
|
|
41
|
+
};
|
|
42
|
+
const deepEqual = (a, b) => {
|
|
43
|
+
if (a === b)
|
|
44
|
+
return true;
|
|
45
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
46
|
+
return false;
|
|
47
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
48
|
+
return false;
|
|
49
|
+
const ka = Object.keys(a);
|
|
50
|
+
if (ka.length !== Object.keys(b).length)
|
|
51
|
+
return false;
|
|
52
|
+
return ka.every(k => deepEqual(a[k], b[k]));
|
|
53
|
+
};
|
|
54
|
+
const getAtPath = (doc, segs) => {
|
|
55
|
+
let cur = doc;
|
|
56
|
+
for (const seg of segs) {
|
|
57
|
+
if (cur === null || typeof cur !== 'object')
|
|
58
|
+
throw new Error('path not found');
|
|
59
|
+
cur = Array.isArray(cur) ? cur[toIndex(seg, cur, false)] : cur[seg];
|
|
60
|
+
if (cur === undefined)
|
|
61
|
+
throw new Error('path not found');
|
|
62
|
+
}
|
|
63
|
+
return cur;
|
|
64
|
+
};
|
|
65
|
+
/** Portable deep clone for the rare `copy` op (structuredClone needs node >= 17). */
|
|
66
|
+
const deepClone = (v) => typeof structuredClone === 'function' ? structuredClone(v) : JSON.parse(JSON.stringify(v));
|
|
67
|
+
export function applyPatchImmutable(doc, ops) {
|
|
68
|
+
// Containers created by THIS batch — safe to mutate in place. Keyed by the
|
|
69
|
+
// copies themselves, so shared history from the input doc is never touched.
|
|
70
|
+
const cloned = new Set();
|
|
71
|
+
/** Return a batch-owned version of `container` (copying it if needed). */
|
|
72
|
+
const own = (container) => {
|
|
73
|
+
if (cloned.has(container))
|
|
74
|
+
return container;
|
|
75
|
+
const copy = (Array.isArray(container) ? container.slice() : Object.assign({}, container));
|
|
76
|
+
cloned.add(copy);
|
|
77
|
+
return copy;
|
|
78
|
+
};
|
|
79
|
+
let root = doc;
|
|
80
|
+
const applyOne = (op) => {
|
|
81
|
+
var _a, _b, _c;
|
|
82
|
+
const segs = parsePointer(op.path);
|
|
83
|
+
// Whole-document operations.
|
|
84
|
+
if (segs.length === 0) {
|
|
85
|
+
switch (op.op) {
|
|
86
|
+
case 'replace':
|
|
87
|
+
case 'add':
|
|
88
|
+
root = op.value;
|
|
89
|
+
return;
|
|
90
|
+
case 'test':
|
|
91
|
+
if (!deepEqual(root, op.value))
|
|
92
|
+
throw new Error('test failed at root');
|
|
93
|
+
return;
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`op "${op.op}" not allowed on document root`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// move/copy decompose into get + remove/add. Reusing the moved reference
|
|
99
|
+
// is safe (it ends up at exactly one location); copy inserts a detached
|
|
100
|
+
// clone so a later op on one copy can never alias into the other.
|
|
101
|
+
if (op.op === 'move') {
|
|
102
|
+
const value = getAtPath(root, parsePointer((_a = op.from) !== null && _a !== void 0 ? _a : ''));
|
|
103
|
+
applyOne({ op: 'remove', path: (_b = op.from) !== null && _b !== void 0 ? _b : '' });
|
|
104
|
+
applyOne({ op: 'add', path: op.path, value });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (op.op === 'copy') {
|
|
108
|
+
applyOne({ op: 'add', path: op.path, value: deepClone(getAtPath(root, parsePointer((_c = op.from) !== null && _c !== void 0 ? _c : ''))) });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Copy-on-write walk: own every container from the root down to the
|
|
112
|
+
// operation's parent, re-linking each owned copy into its (owned) parent.
|
|
113
|
+
root = own(root);
|
|
114
|
+
let parent = root;
|
|
115
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
116
|
+
const key = Array.isArray(parent) ? toIndex(segs[i], parent, false) : segs[i];
|
|
117
|
+
const child = parent[key];
|
|
118
|
+
if (child === null || typeof child !== 'object')
|
|
119
|
+
throw new Error(`path not found: ${op.path}`);
|
|
120
|
+
const owned = own(child);
|
|
121
|
+
if (owned !== child)
|
|
122
|
+
parent[key] = owned; // parent is owned → safe
|
|
123
|
+
parent = owned;
|
|
124
|
+
}
|
|
125
|
+
const last = segs[segs.length - 1];
|
|
126
|
+
switch (op.op) {
|
|
127
|
+
case 'replace':
|
|
128
|
+
if (Array.isArray(parent))
|
|
129
|
+
parent[toIndex(last, parent, false)] = op.value;
|
|
130
|
+
else if (last in parent)
|
|
131
|
+
parent[last] = op.value;
|
|
132
|
+
else
|
|
133
|
+
throw new Error(`replace target missing: ${op.path}`);
|
|
134
|
+
return;
|
|
135
|
+
case 'add':
|
|
136
|
+
if (Array.isArray(parent))
|
|
137
|
+
parent.splice(toIndex(last, parent, true), 0, op.value);
|
|
138
|
+
else
|
|
139
|
+
parent[last] = op.value;
|
|
140
|
+
return;
|
|
141
|
+
case 'remove':
|
|
142
|
+
if (Array.isArray(parent))
|
|
143
|
+
parent.splice(toIndex(last, parent, false), 1);
|
|
144
|
+
else if (last in parent)
|
|
145
|
+
delete parent[last];
|
|
146
|
+
else
|
|
147
|
+
throw new Error(`remove target missing: ${op.path}`);
|
|
148
|
+
return;
|
|
149
|
+
case 'test': {
|
|
150
|
+
const actual = Array.isArray(parent) ? parent[toIndex(last, parent, false)] : parent[last];
|
|
151
|
+
if (!deepEqual(actual, op.value))
|
|
152
|
+
throw new Error(`test failed: ${op.path}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
throw new Error(`unknown op: ${op.op}`);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
for (const op of ops)
|
|
160
|
+
applyOne(op);
|
|
161
|
+
return root;
|
|
162
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import fastpatch from 'fast-json-patch';
|
|
2
|
-
import jsonpatch from 'jsonpatch';
|
|
3
1
|
import * as THREE from 'three';
|
|
4
2
|
import { DEFAULT_STRUCTURE_TREE } from '../../types';
|
|
5
3
|
import { MathUtils } from '../../utils/Math';
|
|
4
|
+
import { applyPatchImmutable } from './immutablePatch';
|
|
6
5
|
////////////////////////////////////////////////////
|
|
7
6
|
// DATA
|
|
8
7
|
////////////////////////////////////////////////////
|
|
@@ -28,28 +27,33 @@ export const structureApi = (set, get, buerliApi) => ({
|
|
|
28
27
|
// console.log('cleanup')
|
|
29
28
|
api.structure.postProcessStructureUpdate();
|
|
30
29
|
},
|
|
30
|
+
/**
|
|
31
|
+
* Applies structure patches to the tree. Returns `false` when a patch block
|
|
32
|
+
* failed to apply — the tree is then diverged from the server's baseline and
|
|
33
|
+
* stays diverged (patches are relative), so the caller should recover by
|
|
34
|
+
* re-fetching the full tree.
|
|
35
|
+
*/
|
|
31
36
|
applyPatches: (patches) => {
|
|
32
37
|
const before = get().structure.products.concat(get().structure.visibleContainers);
|
|
38
|
+
let ok = true;
|
|
33
39
|
const applyBlock = (block) => {
|
|
34
40
|
if (block.length === 0)
|
|
35
41
|
return;
|
|
36
42
|
set(state => {
|
|
37
43
|
let structure = state.structure;
|
|
38
44
|
try {
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
structure = jsonpatch.apply_patch(state.structure, block);
|
|
47
|
-
}
|
|
45
|
+
// Structural-sharing patch application: only containers on a patched
|
|
46
|
+
// path get new identities, untouched subtrees stay reference-equal —
|
|
47
|
+
// so identity-sensitive subscribers/effects only react to genuine
|
|
48
|
+
// changes. Replaces the former jsonpatch/fast-json-patch split (the
|
|
49
|
+
// > 100 ops fast-json-patch path deep-cloned the WHOLE document and
|
|
50
|
+
// gave every node a new identity on every large block).
|
|
51
|
+
structure = applyPatchImmutable(state.structure, block);
|
|
48
52
|
}
|
|
49
53
|
catch (e) {
|
|
54
|
+
ok = false;
|
|
50
55
|
console.error(e);
|
|
51
56
|
}
|
|
52
|
-
// console.info('currentProduct in new patched structure: ', structure.currentProduct)
|
|
53
57
|
return { structure: Object.assign(Object.assign({}, state.structure), structure) };
|
|
54
58
|
});
|
|
55
59
|
};
|
|
@@ -81,6 +85,7 @@ export const structureApi = (set, get, buerliApi) => ({
|
|
|
81
85
|
if (before.sort().join() !== after.sort().join()) {
|
|
82
86
|
api.geometry.updateBounds();
|
|
83
87
|
}
|
|
88
|
+
return ok;
|
|
84
89
|
},
|
|
85
90
|
filterTree: (tree, filter) => {
|
|
86
91
|
const objs = [];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* applyPatchImmutable — RFC 6902 (JSON Patch) applied with structural sharing.
|
|
3
|
+
*
|
|
4
|
+
* Never mutates the input document. Copies exactly the containers on the path
|
|
5
|
+
* of each operation (copy-on-write); untouched siblings keep their identity,
|
|
6
|
+
* so store subscribers with reference-equality selectors only wake where
|
|
7
|
+
* something actually changed.
|
|
8
|
+
*
|
|
9
|
+
* The speed trick: a per-batch `cloned` set tracks containers that are already
|
|
10
|
+
* fresh copies owned by this batch — those are mutated in place. A batch of
|
|
11
|
+
* N ops therefore costs one shallow copy per DISTINCT touched container (plus
|
|
12
|
+
* the walk), not one copy chain per op. No full deep clone, ever.
|
|
13
|
+
*
|
|
14
|
+
* This replaces the previous two-library setup (jsonpatch for small blocks,
|
|
15
|
+
* fast-json-patch with a full deep clone for blocks > 100 ops): it is faster
|
|
16
|
+
* than either in its own domain AND keeps identities correct, so the 100-op
|
|
17
|
+
* threshold and its discontinuous identity behavior are gone.
|
|
18
|
+
*
|
|
19
|
+
* Supported ops: add, remove, replace, move, copy, test (the full RFC set;
|
|
20
|
+
* the engine's json::diff emits only add/remove/replace). Throws on invalid
|
|
21
|
+
* paths/ops — the caller decides how to recover, exactly like the previous
|
|
22
|
+
* jsonpatch behavior.
|
|
23
|
+
*/
|
|
24
|
+
export declare type JsonPatchOp = {
|
|
25
|
+
op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test';
|
|
26
|
+
path: string;
|
|
27
|
+
value?: unknown;
|
|
28
|
+
from?: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function applyPatchImmutable<T>(doc: T, ops: JsonPatchOp[]): T;
|
|
@@ -12,7 +12,13 @@ export declare const structureData: () => StructureData;
|
|
|
12
12
|
export declare const structureApi: (set: StoreApi<DrawingState>['setState'], get: StoreApi<DrawingState>['getState'], buerliApi: StoreApi<BuerliState>) => {
|
|
13
13
|
get: () => StructureData;
|
|
14
14
|
set: (structure: IStructureProtocol) => void;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Applies structure patches to the tree. Returns `false` when a patch block
|
|
17
|
+
* failed to apply — the tree is then diverged from the server's baseline and
|
|
18
|
+
* stays diverged (patches are relative), so the caller should recover by
|
|
19
|
+
* re-fetching the full tree.
|
|
20
|
+
*/
|
|
21
|
+
applyPatches: (patches: IStructurePatch[]) => boolean;
|
|
16
22
|
filterTree: (tree: IStructureTree, filter: (obj: IStructureObject) => boolean) => IStructureObject[];
|
|
17
23
|
findTree: (tree: IStructureTree, filter: (obj: IStructureObject) => boolean) => IStructureObject | undefined;
|
|
18
24
|
collectUsedSolidIds: () => ObjectID[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buerli.io/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "buerli state management",
|
|
5
5
|
"author": "AWV Informatik AG <info@awv-informatik.ch>",
|
|
6
6
|
"license": "ISC",
|
|
@@ -29,13 +29,11 @@
|
|
|
29
29
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
|
30
30
|
"jest": "^26.1.0",
|
|
31
31
|
"rimraf": "^2.6.3",
|
|
32
|
-
"three": "^0.
|
|
32
|
+
"three": "^0.153",
|
|
33
33
|
"typescript": "4.8.4"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@classcad/api-js": "https://awvstatic.com/classcad/download/release/21.2.0/js/classcad-api-js-21.2.0.tgz",
|
|
37
|
-
"fast-json-patch": "^3.1.1",
|
|
38
|
-
"jsonpatch": "^3.1.0",
|
|
39
37
|
"uuid": "^9.0.0",
|
|
40
38
|
"zustand": "^4.1.4"
|
|
41
39
|
},
|