@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.
@@ -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
- // TODO: keep an eye on the patch behaviour in general.
70
- // For large changes 'fast-json-patch' is much more performant than 'jsonpatch'.
71
- // However, for smaller changes 'jsonpatch' is more performant than 'fast-json-patch'.
72
- if (block.length > 100) {
73
- structure = fast_json_patch_1.default.applyPatch(state.structure, block, false, false).newDocument;
74
- }
75
- else {
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
- // TODO: keep an eye on the patch behaviour in general.
40
- // For large changes 'fast-json-patch' is much more performant than 'jsonpatch'.
41
- // However, for smaller changes 'jsonpatch' is more performant than 'fast-json-patch'.
42
- if (block.length > 100) {
43
- structure = fastpatch.applyPatch(state.structure, block, false, false).newDocument;
44
- }
45
- else {
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
- applyPatches: (patches: IStructurePatch[]) => void;
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.1.0-beta.8",
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.165",
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
  },