@rasamjs/core 0.3.0 → 0.3.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/CHANGELOG.md +12 -0
- package/README.md +7 -0
- package/dist/changes/restore.js +54 -24
- package/dist/changes/restore.js.map +1 -1
- package/dist/changes/service.js +2 -2
- package/dist/changes/service.js.map +1 -1
- package/dist/changes/state.d.ts.map +1 -1
- package/dist/changes/state.js +33 -27
- package/dist/changes/state.js.map +1 -1
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +20 -4
- package/dist/changes.js.map +1 -1
- package/dist/components/decorators.d.ts.map +1 -1
- package/dist/components/decorators.js +4 -2
- package/dist/components/decorators.js.map +1 -1
- package/dist/services/claims.d.ts +7 -0
- package/dist/services/claims.d.ts.map +1 -0
- package/dist/services/claims.js +11 -0
- package/dist/services/claims.js.map +1 -0
- package/dist/services/registry.d.ts.map +1 -1
- package/dist/services/registry.js +2 -0
- package/dist/services/registry.js.map +1 -1
- package/package.json +157 -156
- package/reports/adoption.json +20 -20
- package/reports/animation-benchmark.json +612 -612
- package/reports/benchmark.json +45 -45
- package/reports/change-groups-benchmark.json +471 -0
- package/reports/creation.json +26 -26
- package/reports/interchange-benchmark.json +85 -85
- package/reports/phase-3-core-after.json +40 -0
- package/reports/phase-3-core-before.json +40 -0
- package/reports/public-api-contract.json +1 -0
- package/reports/size-components.json +3 -2
- package/reports/size-parity.json +9 -6
- package/reports/size.json +3 -2
- package/reports/svgjs-comparison.json +3104 -3104
- package/reports/svgjs-comparison.md +14 -14
- package/reports/svgjs-parity-current.json +43 -49
- package/src/changes/restore.ts +51 -32
- package/src/changes/service.ts +2 -2
- package/src/changes/state.ts +37 -28
- package/src/changes.ts +24 -5
- package/src/components/decorators.ts +4 -2
- package/src/services/claims.ts +14 -0
- package/src/services/registry.ts +2 -0
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
# Rasam vs SVG.js
|
|
2
2
|
|
|
3
|
-
SVG.js 3.2.8; Rasam 0.3.
|
|
3
|
+
SVG.js 3.2.8; Rasam 0.3.1 workspace. CPU: Intel(R) Core(TM) i7-10610U CPU @ 1.80GHz.
|
|
4
4
|
|
|
5
5
|
All timings below are medians in milliseconds. Ratios above 1 mean Rasam took longer.
|
|
6
6
|
|
|
7
7
|
| Browser | Workload | Nodes | Samples | Rasam | SVG.js | Rasam / SVG.js |
|
|
8
8
|
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
|
9
|
-
| chromium 156.0.8078.4 | creation | 2000 | 15 | 9.
|
|
10
|
-
| chromium 156.0.8078.4 | creation | 8000 | 15 |
|
|
11
|
-
| chromium 156.0.8078.4 | fill | 1000 | 100 | 0.
|
|
12
|
-
| chromium 156.0.8078.4 | repeated-fill | 1000 | 100 | 0.
|
|
13
|
-
| chromium 156.0.8078.4 | pointer-one | 8000 | 40 | 3.
|
|
14
|
-
| chromium 156.0.8078.4 | pointer-all | 8000 | 40 | 3.
|
|
15
|
-
| firefox 157.0 | creation | 2000 | 15 |
|
|
16
|
-
| firefox 157.0 | creation | 8000 | 15 |
|
|
17
|
-
| firefox 157.0 | fill | 1000 | 100 | 1.
|
|
18
|
-
| firefox 157.0 | repeated-fill | 1000 | 100 | 1.
|
|
9
|
+
| chromium 156.0.8078.4 | creation | 2000 | 15 | 9.50 | 10.90 | 0.87 |
|
|
10
|
+
| chromium 156.0.8078.4 | creation | 8000 | 15 | 37.80 | 40.00 | 0.95 |
|
|
11
|
+
| chromium 156.0.8078.4 | fill | 1000 | 100 | 0.49 | 1.31 | 0.37 |
|
|
12
|
+
| chromium 156.0.8078.4 | repeated-fill | 1000 | 100 | 0.70 | 3.56 | 0.20 |
|
|
13
|
+
| chromium 156.0.8078.4 | pointer-one | 8000 | 40 | 3.87 | 3.68 | 1.05 |
|
|
14
|
+
| chromium 156.0.8078.4 | pointer-all | 8000 | 40 | 3.83 | 3.66 | 1.05 |
|
|
15
|
+
| firefox 157.0 | creation | 2000 | 15 | 23.00 | 21.00 | 1.10 |
|
|
16
|
+
| firefox 157.0 | creation | 8000 | 15 | 91.00 | 82.00 | 1.11 |
|
|
17
|
+
| firefox 157.0 | fill | 1000 | 100 | 1.30 | 1.50 | 0.87 |
|
|
18
|
+
| firefox 157.0 | repeated-fill | 1000 | 100 | 1.60 | 4.50 | 0.36 |
|
|
19
19
|
| firefox 157.0 | pointer-one | 8000 | 40 | 4.20 | 3.40 | 1.24 |
|
|
20
20
|
| firefox 157.0 | pointer-all | 8000 | 40 | 4.00 | 3.40 | 1.18 |
|
|
21
|
-
| webkit 27.2 | creation | 2000 | 15 |
|
|
22
|
-
| webkit 27.2 | creation | 8000 | 15 |
|
|
23
|
-
| webkit 27.2 | fill | 1000 | 100 | 0.70 | 1.
|
|
21
|
+
| webkit 27.2 | creation | 2000 | 15 | 14.00 | 18.00 | 0.78 |
|
|
22
|
+
| webkit 27.2 | creation | 8000 | 15 | 58.00 | 73.00 | 0.79 |
|
|
23
|
+
| webkit 27.2 | fill | 1000 | 100 | 0.70 | 1.60 | 0.44 |
|
|
24
24
|
| webkit 27.2 | repeated-fill | 1000 | 100 | 0.90 | 4.90 | 0.18 |
|
|
25
25
|
| webkit 27.2 | pointer-one | 8000 | 40 | 4.20 | 3.40 | 1.24 |
|
|
26
26
|
| webkit 27.2 | pointer-all | 8000 | 40 | 4.20 | 3.40 | 1.24 |
|
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-11-
|
|
3
|
-
"revision": "
|
|
2
|
+
"generatedAt": "2026-11-09T00:19:11.349Z",
|
|
3
|
+
"revision": "124aaed60d894f51033904e9959bbf4286d0f3e9",
|
|
4
4
|
"dirtyTree": [
|
|
5
|
-
"M
|
|
6
|
-
" M
|
|
7
|
-
" M
|
|
8
|
-
" M bun.lock",
|
|
9
|
-
" M packages/boolean/package.json",
|
|
10
|
-
" M packages/clipboard/package.json",
|
|
11
|
-
" M packages/connectors/API.md",
|
|
12
|
-
" M packages/connectors/CHANGELOG.md",
|
|
13
|
-
" M packages/connectors/README.md",
|
|
14
|
-
" M packages/connectors/RELEASING.md",
|
|
15
|
-
" M packages/connectors/package.json",
|
|
16
|
-
" M packages/editor/package.json",
|
|
17
|
-
" M packages/history/package.json",
|
|
18
|
-
" M packages/rasam/CHANGELOG.md",
|
|
19
|
-
" M packages/rasam/RELEASING.md",
|
|
20
|
-
" M packages/rasam/package.json",
|
|
5
|
+
"M packages/editor/reports/release-readiness.json",
|
|
6
|
+
" M packages/history/reports/history-performance.json",
|
|
7
|
+
" M packages/history/reports/release-readiness.json",
|
|
21
8
|
" M packages/rasam/reports/adoption.json",
|
|
9
|
+
" M packages/rasam/reports/animation-benchmark.json",
|
|
10
|
+
" M packages/rasam/reports/benchmark.json",
|
|
22
11
|
" M packages/rasam/reports/creation.json",
|
|
23
12
|
" M packages/rasam/reports/interchange-benchmark.json",
|
|
24
|
-
" M packages/
|
|
25
|
-
" M packages/
|
|
26
|
-
" M packages/
|
|
27
|
-
" M packages/
|
|
28
|
-
" M packages/
|
|
13
|
+
" M packages/rasam/reports/release-readiness.json",
|
|
14
|
+
" M packages/rasam/reports/svgjs-comparison.json",
|
|
15
|
+
" M packages/rasam/reports/svgjs-comparison.md",
|
|
16
|
+
" M packages/rasam/reports/svgjs-parity-current.json",
|
|
17
|
+
" M packages/selection/reports/marquee-benchmark.json",
|
|
18
|
+
" M packages/selection/reports/release-readiness.json",
|
|
19
|
+
" M packages/tools/reports/drag-benchmark.json",
|
|
20
|
+
" M packages/tools/reports/release-readiness.json"
|
|
29
21
|
],
|
|
30
22
|
"sourceFiles": [
|
|
31
23
|
{
|
|
@@ -98,7 +90,7 @@
|
|
|
98
90
|
},
|
|
99
91
|
{
|
|
100
92
|
"path": "src/changes.ts",
|
|
101
|
-
"sha256": "
|
|
93
|
+
"sha256": "5195b06c773565fb7fcc79b14249eaef931e0fa79d0f7631f5ad1bec50a7f6af"
|
|
102
94
|
},
|
|
103
95
|
{
|
|
104
96
|
"path": "src/changes/derived.ts",
|
|
@@ -110,15 +102,15 @@
|
|
|
110
102
|
},
|
|
111
103
|
{
|
|
112
104
|
"path": "src/changes/restore.ts",
|
|
113
|
-
"sha256": "
|
|
105
|
+
"sha256": "f2f3f749888b62b44a0667a7141b72786d488622740882841a6f5f08be8e3a9a"
|
|
114
106
|
},
|
|
115
107
|
{
|
|
116
108
|
"path": "src/changes/service.ts",
|
|
117
|
-
"sha256": "
|
|
109
|
+
"sha256": "0384e84ab0fcaef38f67bd2167c498e1188fc639ef23f94aeed45e3895c606a5"
|
|
118
110
|
},
|
|
119
111
|
{
|
|
120
112
|
"path": "src/changes/state.ts",
|
|
121
|
-
"sha256": "
|
|
113
|
+
"sha256": "889ace35fb6ca0ddb11960c43e24b64bfb96697165b8d9ea5006aae99b0ec18a"
|
|
122
114
|
},
|
|
123
115
|
{
|
|
124
116
|
"path": "src/changes/types.ts",
|
|
@@ -138,7 +130,7 @@
|
|
|
138
130
|
},
|
|
139
131
|
{
|
|
140
132
|
"path": "src/components/decorators.ts",
|
|
141
|
-
"sha256": "
|
|
133
|
+
"sha256": "5a94b137073c4f41b7bdea53871b9b8270aa731bfe49963b3209717b35673937"
|
|
142
134
|
},
|
|
143
135
|
{
|
|
144
136
|
"path": "src/components/metadata.ts",
|
|
@@ -612,6 +604,10 @@
|
|
|
612
604
|
"path": "src/services/catalog.ts",
|
|
613
605
|
"sha256": "bd11868233bb9604062311cff9bfbacf2cf93e054dd979b5c0853e3b439ad7b2"
|
|
614
606
|
},
|
|
607
|
+
{
|
|
608
|
+
"path": "src/services/claims.ts",
|
|
609
|
+
"sha256": "b8db85df1c0f0806d30b6caccf20f1a94d14c2975a01b11e2ef5fa411080c817"
|
|
610
|
+
},
|
|
615
611
|
{
|
|
616
612
|
"path": "src/services/context.ts",
|
|
617
613
|
"sha256": "3d86040e3a601321d4811b17f1063a0e181e3d6362918a6d0bf298da19c0f72f"
|
|
@@ -638,7 +634,7 @@
|
|
|
638
634
|
},
|
|
639
635
|
{
|
|
640
636
|
"path": "src/services/registry.ts",
|
|
641
|
-
"sha256": "
|
|
637
|
+
"sha256": "a6ad5e6fcc646b10349c29ed0163863b958892e2a008c033eec18fd658a53f1a"
|
|
642
638
|
},
|
|
643
639
|
{
|
|
644
640
|
"path": "src/services/scheduler.ts",
|
|
@@ -742,7 +738,7 @@
|
|
|
742
738
|
},
|
|
743
739
|
{
|
|
744
740
|
"path": "../../bun.lock",
|
|
745
|
-
"sha256": "
|
|
741
|
+
"sha256": "b3789d00332812b09c75e008611f9bd5c7721c296c8a883a3c7a71cde94d3193"
|
|
746
742
|
}
|
|
747
743
|
],
|
|
748
744
|
"svgjs": "3.2.8",
|
|
@@ -9722,8 +9718,8 @@
|
|
|
9722
9718
|
},
|
|
9723
9719
|
"size": {
|
|
9724
9720
|
"fixture": "fixtures/size.ts",
|
|
9725
|
-
"rawBytes":
|
|
9726
|
-
"gzipBytes":
|
|
9721
|
+
"rawBytes": 68463,
|
|
9722
|
+
"gzipBytes": 20572,
|
|
9727
9723
|
"budgetBytes": 20992,
|
|
9728
9724
|
"included": [
|
|
9729
9725
|
"src/changes/mutation.ts",
|
|
@@ -9762,6 +9758,7 @@
|
|
|
9762
9758
|
"src/core/native-validation.ts",
|
|
9763
9759
|
"src/services/element-key.ts",
|
|
9764
9760
|
"src/services/initialization.ts",
|
|
9761
|
+
"src/services/claims.ts",
|
|
9765
9762
|
"src/services/registry.ts",
|
|
9766
9763
|
"src/services/scheduler.ts",
|
|
9767
9764
|
"src/changes/service.ts",
|
|
@@ -9775,7 +9772,7 @@
|
|
|
9775
9772
|
},
|
|
9776
9773
|
"performance": {
|
|
9777
9774
|
"fixture": "scripts/benchmark.mjs",
|
|
9778
|
-
"date": "2026-11-
|
|
9775
|
+
"date": "2026-11-09T00:19:06.376Z",
|
|
9779
9776
|
"browser": "Chromium 156.0.8078.4",
|
|
9780
9777
|
"node": "v24.21.0",
|
|
9781
9778
|
"os": "win32 10.0.26200",
|
|
@@ -9784,30 +9781,27 @@
|
|
|
9784
9781
|
"count": 1000,
|
|
9785
9782
|
"warmup": 20,
|
|
9786
9783
|
"iterations": 100,
|
|
9787
|
-
"medianMs": 0.
|
|
9788
|
-
"p95Ms":
|
|
9784
|
+
"medianMs": 0.5,
|
|
9785
|
+
"p95Ms": 0.7999999523162842,
|
|
9789
9786
|
"targetMedianMs": 16,
|
|
9790
9787
|
"includes": "logical mutations and synchronous DOM commit; excludes paint",
|
|
9791
9788
|
"timings": [
|
|
9792
|
-
0.
|
|
9793
|
-
0.
|
|
9789
|
+
0.3999999761581421, 0.3999999761581421, 0.3999999761581421, 0.3999999761581421,
|
|
9790
|
+
0.40000009536743164, 0.40000009536743164, 0.40000009536743164, 0.40000009536743164, 0.5, 0.5,
|
|
9791
|
+
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
|
|
9792
|
+
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
|
|
9793
|
+
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5999999046325684, 0.5999999046325684, 0.5999999046325684,
|
|
9794
9794
|
0.5999999046325684, 0.5999999046325684, 0.5999999046325684, 0.5999999046325684,
|
|
9795
|
-
0.5999999046325684, 0.
|
|
9795
|
+
0.5999999046325684, 0.5999999046325684, 0.6000000238418579, 0.6000000238418579,
|
|
9796
|
+
0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
|
|
9797
|
+
0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
|
|
9796
9798
|
0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
|
|
9797
9799
|
0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
|
|
9798
|
-
0.6000000238418579, 0.6000000238418579, 0.
|
|
9799
|
-
0.6999999284744263, 0.6999999284744263, 0.6999999284744263, 0.6999999284744263,
|
|
9800
|
-
0.6999999284744263, 0.6999999284744263, 0.6999999284744263, 0.7000000476837158,
|
|
9800
|
+
0.6000000238418579, 0.6000000238418579, 0.6999999284744263, 0.6999999284744263,
|
|
9801
9801
|
0.7000000476837158, 0.7000000476837158, 0.7000000476837158, 0.7000000476837158,
|
|
9802
9802
|
0.7000000476837158, 0.7000000476837158, 0.7000000476837158, 0.7000000476837158,
|
|
9803
|
-
0.7000000476837158, 0.
|
|
9804
|
-
0.8000000715255737, 0.
|
|
9805
|
-
0.8999999761581421, 0.8999999761581421, 0.8999999761581421, 0.9000000953674316, 1, 1, 1, 1,
|
|
9806
|
-
1.100000023841858, 1.100000023841858, 1.100000023841858, 1.100000023841858, 1.100000023841858,
|
|
9807
|
-
1.1999999284744263, 1.1999999284744263, 1.1999999284744263, 1.1999999284744263,
|
|
9808
|
-
1.1999999284744263, 1.2000000476837158, 1.2000000476837158, 1.2000000476837158,
|
|
9809
|
-
1.2000000476837158, 1.2999999523162842, 1.2999999523162842, 1.2999999523162842,
|
|
9810
|
-
1.3000000715255737, 1.3000000715255737, 1.399999976158142, 1.5, 1.5, 1.600000023841858
|
|
9803
|
+
0.7000000476837158, 0.7999999523162842, 0.7999999523162842, 0.7999999523162842,
|
|
9804
|
+
0.8000000715255737, 0.8999999761581421, 1.2999999523162842
|
|
9811
9805
|
]
|
|
9812
9806
|
}
|
|
9813
9807
|
}
|
package/src/changes/restore.ts
CHANGED
|
@@ -68,8 +68,8 @@ function applyStateChange(
|
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
return { ...snapshot, [
|
|
71
|
+
const field = change.domain === 'bounds' ? 'contributesToBounds' : change.domain;
|
|
72
|
+
return { ...snapshot, [field]: value };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/** Validates the whole patch before writing; rolls back logical state on failure. */
|
|
@@ -186,18 +186,37 @@ function validateGraph(
|
|
|
186
186
|
): void {
|
|
187
187
|
const context = owner.context;
|
|
188
188
|
const removed = new Set([...states].filter(([, state]) => !state).map(([target]) => target.node));
|
|
189
|
+
let ids: Map<unknown, AnyNode | null> | undefined;
|
|
190
|
+
const siblingCounts = new Map<NodeHandle, number>();
|
|
191
|
+
for (const state of states.values()) {
|
|
192
|
+
const parent = state?.placement.parent;
|
|
193
|
+
if (!parent) continue;
|
|
194
|
+
let count = siblingCounts.get(parent);
|
|
195
|
+
if (count === undefined) {
|
|
196
|
+
count = 0;
|
|
197
|
+
if (!parent.node.isDestroyed && 'children' in parent.node)
|
|
198
|
+
for (const node of (parent.node as ParentNode).children)
|
|
199
|
+
if (!states.has(context.changes.handle(node))) count++;
|
|
200
|
+
}
|
|
201
|
+
siblingCounts.set(parent, count + 1);
|
|
202
|
+
}
|
|
189
203
|
for (const [target, state] of states) {
|
|
190
204
|
if (!state) continue;
|
|
191
205
|
if (context.registry.registered(state.constructor) !== state.constructor)
|
|
192
206
|
throw new ChangeConflictError(`Constructor is not registered: ${state.constructor.tag}`);
|
|
193
207
|
const id = state.properties.id?.value;
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
if (id !== undefined) {
|
|
209
|
+
if (!ids) {
|
|
210
|
+
ids = new Map();
|
|
211
|
+
for (const node of context.registry.nodes) {
|
|
212
|
+
const value = node.getAttributeValue('id');
|
|
213
|
+
if (value !== undefined && !removed.has(node))
|
|
214
|
+
ids.set(value, ids.has(value) ? null : node);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (ids.has(id) && ids.get(id) !== target.node)
|
|
218
|
+
throw new ChangeConflictError(`SVG ID is already in use: ${id}`);
|
|
219
|
+
}
|
|
201
220
|
if (!target.node.isDestroyed) {
|
|
202
221
|
for (const [attribute, property] of Object.entries(state.properties))
|
|
203
222
|
target.node.validateAttribute(attribute, property.value);
|
|
@@ -221,22 +240,10 @@ function validateGraph(
|
|
|
221
240
|
throw new ChangeConflictError('The reference value and its planned target ID disagree');
|
|
222
241
|
}
|
|
223
242
|
if (state.placement.parent) {
|
|
224
|
-
const parent = state.placement.parent;
|
|
225
|
-
const existing =
|
|
226
|
-
!parent.node.isDestroyed && 'children' in parent.node
|
|
227
|
-
? (parent.node as ParentNode).children.map((node) => context.changes.handle(node))
|
|
228
|
-
: [];
|
|
229
|
-
const children = new Set(
|
|
230
|
-
existing.filter(
|
|
231
|
-
(child) => !states.has(child) || states.get(child)?.placement.parent === parent,
|
|
232
|
-
),
|
|
233
|
-
);
|
|
234
|
-
for (const [child, planned] of states)
|
|
235
|
-
if (planned?.placement.parent === parent) children.add(child);
|
|
236
243
|
if (
|
|
237
244
|
!Number.isInteger(state.placement.index) ||
|
|
238
245
|
state.placement.index < 0 ||
|
|
239
|
-
state.placement.index >=
|
|
246
|
+
state.placement.index >= siblingCounts.get(state.placement.parent)!
|
|
240
247
|
)
|
|
241
248
|
throw new ChangeConflictError('The history sibling position is no longer available');
|
|
242
249
|
}
|
|
@@ -342,23 +349,35 @@ function applyStates(
|
|
|
342
349
|
domains.get(target)!.has('existence')),
|
|
343
350
|
);
|
|
344
351
|
for (const [target] of placements) if (target.node.parent) target.node.remove();
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
352
|
+
const positions = new Map(placements.map(([target], index) => [target, index]));
|
|
353
|
+
const dependants = new Map<NodeHandle, typeof placements>();
|
|
354
|
+
let ready: typeof placements = [];
|
|
355
|
+
for (const entry of placements) {
|
|
356
|
+
const parent = entry[1]!.placement.parent;
|
|
357
|
+
if (!parent || !positions.has(parent)) ready.push(entry);
|
|
358
|
+
else {
|
|
359
|
+
let children = dependants.get(parent);
|
|
360
|
+
if (!children) dependants.set(parent, (children = []));
|
|
361
|
+
children.push(entry);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Preflight has already rejected parent cycles.
|
|
365
|
+
while (ready.length) {
|
|
366
|
+
ready.sort(
|
|
367
|
+
(a, b) =>
|
|
368
|
+
a[1]!.placement.index - b[1]!.placement.index ||
|
|
369
|
+
positions.get(a[0])! - positions.get(b[0])!,
|
|
350
370
|
);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
for (const entry of ready) {
|
|
354
|
-
const [target, state] = entry;
|
|
371
|
+
const next: typeof placements = [];
|
|
372
|
+
for (const [target, state] of ready) {
|
|
355
373
|
const parent = state!.placement.parent?.node as ParentNode | undefined;
|
|
356
374
|
if (parent) {
|
|
357
375
|
parent[validateChild](target.node);
|
|
358
376
|
parent.add(target.node, Math.min(state!.placement.index, parent.children.length));
|
|
359
377
|
}
|
|
360
|
-
|
|
378
|
+
for (const child of dependants.get(target) ?? []) next.push(child);
|
|
361
379
|
}
|
|
380
|
+
ready = next;
|
|
362
381
|
}
|
|
363
382
|
// Parent order is authoritative across compound edits and separate edit callbacks.
|
|
364
383
|
for (const [target, state] of states)
|
package/src/changes/service.ts
CHANGED
|
@@ -405,8 +405,7 @@ export class ChangeService {
|
|
|
405
405
|
this.delivering = true;
|
|
406
406
|
const failures: unknown[] = [];
|
|
407
407
|
try {
|
|
408
|
-
|
|
409
|
-
const next = this.queue.shift()!;
|
|
408
|
+
for (const next of this.queue) {
|
|
410
409
|
for (const listener of [...this.listeners]) {
|
|
411
410
|
if (!this.listeners.has(listener)) continue;
|
|
412
411
|
try {
|
|
@@ -417,6 +416,7 @@ export class ChangeService {
|
|
|
417
416
|
}
|
|
418
417
|
}
|
|
419
418
|
} finally {
|
|
419
|
+
this.queue.length = 0;
|
|
420
420
|
this.delivering = false;
|
|
421
421
|
}
|
|
422
422
|
if (failures.length === 1 && failures[0] instanceof ChangeConflictError) throw failures[0];
|
package/src/changes/state.ts
CHANGED
|
@@ -93,25 +93,27 @@ export function diffState(
|
|
|
93
93
|
if (!sameState(previous, value))
|
|
94
94
|
changes.push({ target, domain: 'property', attribute, before: previous, after: value });
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
96
|
+
for (const domain of [
|
|
97
|
+
'transform',
|
|
98
|
+
'content',
|
|
99
|
+
'presentation',
|
|
100
|
+
'custom',
|
|
101
|
+
'native',
|
|
102
|
+
'placement',
|
|
103
|
+
'children',
|
|
104
|
+
] as const) {
|
|
105
|
+
const previous = before[domain],
|
|
106
|
+
value = after[domain];
|
|
107
|
+
if (
|
|
108
|
+
domain !== 'presentation' &&
|
|
109
|
+
domain !== 'custom' &&
|
|
110
|
+
domain !== 'placement' &&
|
|
111
|
+
(!previous || !value)
|
|
112
|
+
)
|
|
113
|
+
continue;
|
|
114
|
+
if (domain === 'transform' ? transform : !sameState(previous, value))
|
|
115
|
+
changes.push({ target, domain, before: previous, after: value } as ModelChange);
|
|
116
|
+
}
|
|
115
117
|
if (before.contributesToBounds !== after.contributesToBounds)
|
|
116
118
|
changes.push({
|
|
117
119
|
target,
|
|
@@ -138,7 +140,7 @@ export function combineChanges(changes: readonly ModelChange[]): readonly ModelC
|
|
|
138
140
|
for (const [target, records] of lifecycle) {
|
|
139
141
|
const first = records.findIndex((change) => change.domain === 'existence');
|
|
140
142
|
if (first < 0) {
|
|
141
|
-
normalized.push(
|
|
143
|
+
for (const record of records) normalized.push(record);
|
|
142
144
|
continue;
|
|
143
145
|
}
|
|
144
146
|
let last = records.length - 1;
|
|
@@ -160,14 +162,22 @@ export function combineChanges(changes: readonly ModelChange[]): readonly ModelC
|
|
|
160
162
|
}
|
|
161
163
|
const orders = new Map<
|
|
162
164
|
NodeHandle,
|
|
163
|
-
{ before:
|
|
165
|
+
{ before: ReadonlyMap<NodeHandle, number>; after: ReadonlyMap<NodeHandle, number> }
|
|
164
166
|
>();
|
|
167
|
+
const indices = (children: readonly NodeHandle[]) => {
|
|
168
|
+
const positions = new Map<NodeHandle, number>();
|
|
169
|
+
// Reverse traversal retains indexOf's first match even for repeated handles.
|
|
170
|
+
for (let index = children.length - 1; index >= 0; index--)
|
|
171
|
+
positions.set(children[index], index);
|
|
172
|
+
return positions;
|
|
173
|
+
};
|
|
165
174
|
for (const change of merged.values()) {
|
|
166
|
-
if (change.domain === 'children')
|
|
175
|
+
if (change.domain === 'children')
|
|
176
|
+
orders.set(change.target, { before: indices(change.before), after: indices(change.after) });
|
|
167
177
|
if (change.domain === 'existence' && (change.before?.children || change.after?.children))
|
|
168
178
|
orders.set(change.target, {
|
|
169
|
-
before: change.before?.children ?? [],
|
|
170
|
-
after: change.after?.children ?? [],
|
|
179
|
+
before: indices(change.before?.children ?? []),
|
|
180
|
+
after: indices(change.after?.children ?? []),
|
|
171
181
|
});
|
|
172
182
|
}
|
|
173
183
|
const placement = (
|
|
@@ -176,7 +186,7 @@ export function combineChanges(changes: readonly ModelChange[]): readonly ModelC
|
|
|
176
186
|
edge: 'before' | 'after',
|
|
177
187
|
) => {
|
|
178
188
|
const siblings = value.parent ? orders.get(value.parent)?.[edge] : undefined;
|
|
179
|
-
return siblings ? { ...value, index: siblings.
|
|
189
|
+
return siblings ? { ...value, index: siblings.get(target) ?? -1 } : value;
|
|
180
190
|
};
|
|
181
191
|
return Object.freeze(
|
|
182
192
|
[...merged.values()]
|
|
@@ -234,9 +244,8 @@ function patchSnapshot(
|
|
|
234
244
|
if (transform.transform !== null) properties.transform = { value: transform.transform };
|
|
235
245
|
return immutable({ ...snapshot, properties, transform });
|
|
236
246
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return immutable({ ...snapshot, [change.domain]: change[edge] });
|
|
247
|
+
const field = change.domain === 'bounds' ? 'contributesToBounds' : change.domain;
|
|
248
|
+
return immutable({ ...snapshot, [field]: change[edge] });
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
export const stateAdapter = Object.freeze({ capture: captureState, diff: diffState, immutable });
|
package/src/changes.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { AnyNode } from './core/types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ChangeEvent,
|
|
4
|
+
ChangeGroup,
|
|
5
|
+
ChangeMetadata,
|
|
6
|
+
ModelChange,
|
|
7
|
+
NodeHandle,
|
|
8
|
+
} from './changes/types.js';
|
|
3
9
|
import { ChangeConflictError, ChangeObserverError } from './changes/types.js';
|
|
4
10
|
import { combineChanges, stateAdapter } from './changes/state.js';
|
|
5
11
|
import { restoreChanges } from './changes/restore.js';
|
|
@@ -64,14 +70,25 @@ export function beginChangeGroup(drawing: AnyNode, metadata: ChangeMetadata = {}
|
|
|
64
70
|
drawing.assertLive();
|
|
65
71
|
const owner = drawing.context.root!;
|
|
66
72
|
let id: number | undefined;
|
|
67
|
-
let changes:
|
|
73
|
+
let changes: ModelChange[] = [];
|
|
74
|
+
const targets = new Set<NodeHandle>();
|
|
68
75
|
let invalidated = false;
|
|
69
76
|
// Subscribe before groupstart so edits made by start observers are captured too.
|
|
70
77
|
const unsubscribe = observeChanges(drawing, (event) => {
|
|
71
78
|
if (event.type === 'dispose') invalidated = true;
|
|
72
79
|
if (id === undefined && event.type === 'groupstart') id = event.group;
|
|
73
|
-
if (event.type
|
|
74
|
-
|
|
80
|
+
if (event.type !== 'change') return;
|
|
81
|
+
if (event.group === id && !event.temporary) {
|
|
82
|
+
for (const change of event.changes) {
|
|
83
|
+
changes.push(change);
|
|
84
|
+
targets.add(change.target);
|
|
85
|
+
}
|
|
86
|
+
} else if (event.changes.some((change) => targets.has(change.target))) {
|
|
87
|
+
// A net-zero field must release its old baseline before an outside edit.
|
|
88
|
+
changes = [...combineChanges(changes)];
|
|
89
|
+
targets.clear();
|
|
90
|
+
for (const change of changes) targets.add(change.target);
|
|
91
|
+
}
|
|
75
92
|
});
|
|
76
93
|
let group: ReturnType<typeof drawing.context.changes.group>;
|
|
77
94
|
try {
|
|
@@ -91,6 +108,7 @@ export function beginChangeGroup(drawing: AnyNode, metadata: ChangeMetadata = {}
|
|
|
91
108
|
const finish = (cancelled: boolean, restored = false) => {
|
|
92
109
|
active = false;
|
|
93
110
|
changes = [];
|
|
111
|
+
targets.clear();
|
|
94
112
|
unsubscribe();
|
|
95
113
|
group.end(cancelled, restored);
|
|
96
114
|
};
|
|
@@ -117,7 +135,8 @@ export function beginChangeGroup(drawing: AnyNode, metadata: ChangeMetadata = {}
|
|
|
117
135
|
const failures: unknown[] = [];
|
|
118
136
|
try {
|
|
119
137
|
try {
|
|
120
|
-
|
|
138
|
+
const combined = combineChanges(changes);
|
|
139
|
+
if (combined.length) restoreChanges(owner, combined, 'undo');
|
|
121
140
|
} catch (error) {
|
|
122
141
|
if (error instanceof ChangeObserverError) failures.push(error);
|
|
123
142
|
else throw error;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { defineComponent } from './metadata.js';
|
|
9
9
|
import { initializeElement } from '../services/initialization.js';
|
|
10
10
|
import { destroySubtrees } from '../core/destruction.js';
|
|
11
|
+
import { claims, claimedSince } from '../services/claims.js';
|
|
11
12
|
import { withInternalStructure } from '../mixins/parent-internals.js';
|
|
12
13
|
import type { ComponentClass, ComponentOptions, PropOptions } from './types.js';
|
|
13
14
|
|
|
@@ -21,13 +22,14 @@ export function component(options: ComponentOptions) {
|
|
|
21
22
|
const Base = target as ComponentClass;
|
|
22
23
|
const Decorated = class extends Base {
|
|
23
24
|
constructor(...args: ConstructorParameters<ComponentClass>) {
|
|
25
|
+
// A marker, not a copy of every node: construction runs once per component.
|
|
24
26
|
const context = args[0],
|
|
25
|
-
|
|
27
|
+
mark = claims;
|
|
26
28
|
try {
|
|
27
29
|
super(...args);
|
|
28
30
|
if (new.target === Decorated && !this.adopting) this[initializeElement]();
|
|
29
31
|
} catch (error) {
|
|
30
|
-
const created = context.registry.nodes
|
|
32
|
+
const created = claimedSince(context.registry.nodes, mark);
|
|
31
33
|
if (created.length) {
|
|
32
34
|
try {
|
|
33
35
|
withInternalStructure(() => created[0][destroySubtrees](created));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyNode } from '../core/types.js';
|
|
2
|
+
|
|
3
|
+
/** Internal claim order. A node keeps the number from its first claim, even if reclaimed. */
|
|
4
|
+
const claimOrder = new WeakMap<AnyNode, number>();
|
|
5
|
+
/** Claims so far: a constant-time marker for `claimedSince`, instead of copying every node. */
|
|
6
|
+
export let claims = 0;
|
|
7
|
+
|
|
8
|
+
export function recordClaim(node: AnyNode): void {
|
|
9
|
+
if (!claimOrder.has(node)) claimOrder.set(node, ++claims);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Registered nodes first claimed after `mark`, in `nodes` order (pass `registry.nodes`). */
|
|
13
|
+
export const claimedSince = (nodes: readonly AnyNode[], mark: number): AnyNode[] =>
|
|
14
|
+
nodes.filter((node) => claimOrder.get(node)! > mark);
|
package/src/services/registry.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { elementKey } from './element-key.js';
|
|
|
9
9
|
import { initializeNode, withInitialization } from './initialization.js';
|
|
10
10
|
import type { DrawingContext } from './context.js';
|
|
11
11
|
import type { ElementClass } from './types.js';
|
|
12
|
+
import { recordClaim } from './claims.js';
|
|
12
13
|
|
|
13
14
|
export class Registry {
|
|
14
15
|
private static readonly identity = new WeakMap<SVGElement, AnyNode>();
|
|
@@ -83,6 +84,7 @@ export class Registry {
|
|
|
83
84
|
if (!this.context.dom.owns(node.el)) throw new Error('Element belongs to another document');
|
|
84
85
|
Registry.identity.set(node.el, node);
|
|
85
86
|
this.wrappers.set(node.el, node);
|
|
87
|
+
recordClaim(node);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
forget(node: AnyNode): void {
|