@rasamjs/core 0.2.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/API.md +14 -4
- package/CHANGELOG.md +71 -0
- package/COMPONENTS.md +229 -0
- package/README.md +46 -6
- package/RELEASING.md +4 -2
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/catalog/fragment.d.ts +2 -2
- package/dist/catalog/fragment.d.ts.map +1 -1
- package/dist/catalog/fragment.js.map +1 -1
- package/dist/changes/derived.d.ts +10 -0
- package/dist/changes/derived.d.ts.map +1 -0
- package/dist/changes/derived.js +14 -0
- package/dist/changes/derived.js.map +1 -0
- package/dist/changes/restore.d.ts.map +1 -1
- package/dist/changes/restore.js +76 -28
- package/dist/changes/restore.js.map +1 -1
- package/dist/changes/service.d.ts +5 -0
- package/dist/changes/service.d.ts.map +1 -1
- package/dist/changes/service.js +18 -4
- 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/changes.d.ts +34 -0
- package/dist/components/changes.d.ts.map +1 -0
- package/dist/components/changes.js +119 -0
- package/dist/components/changes.js.map +1 -0
- package/dist/components/component.d.ts +62 -0
- package/dist/components/component.d.ts.map +1 -0
- package/dist/components/component.js +355 -0
- package/dist/components/component.js.map +1 -0
- package/dist/components/decorators.d.ts +7 -0
- package/dist/components/decorators.d.ts.map +1 -0
- package/dist/components/decorators.js +83 -0
- package/dist/components/decorators.js.map +1 -0
- package/dist/components/metadata.d.ts +10 -0
- package/dist/components/metadata.d.ts.map +1 -0
- package/dist/components/metadata.js +85 -0
- package/dist/components/metadata.js.map +1 -0
- package/dist/components/registry.d.ts +24 -0
- package/dist/components/registry.d.ts.map +1 -0
- package/dist/components/registry.js +78 -0
- package/dist/components/registry.js.map +1 -0
- package/dist/components/types.d.ts +54 -0
- package/dist/components/types.d.ts.map +1 -0
- package/dist/components/types.js +2 -0
- package/dist/components/types.js.map +1 -0
- package/dist/components/view.d.ts +9 -0
- package/dist/components/view.d.ts.map +1 -0
- package/dist/components/view.js +104 -0
- package/dist/components/view.js.map +1 -0
- package/dist/components.d.ts +6 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +5 -0
- package/dist/components.js.map +1 -0
- package/dist/core/node.d.ts +7 -0
- package/dist/core/node.d.ts.map +1 -1
- package/dist/core/node.js +30 -0
- package/dist/core/node.js.map +1 -1
- package/dist/interchange/clone.d.ts.map +1 -1
- package/dist/interchange/clone.js +3 -2
- package/dist/interchange/clone.js.map +1 -1
- package/dist/interchange/reconstruct.d.ts.map +1 -1
- package/dist/interchange/reconstruct.js +16 -12
- package/dist/interchange/reconstruct.js.map +1 -1
- package/dist/interchange/references.d.ts.map +1 -1
- package/dist/interchange/references.js +10 -4
- package/dist/interchange/references.js.map +1 -1
- package/dist/mixins/parent-internals.d.ts +6 -0
- package/dist/mixins/parent-internals.d.ts.map +1 -1
- package/dist/mixins/parent-internals.js +34 -0
- package/dist/mixins/parent-internals.js.map +1 -1
- package/dist/mixins/parent.d.ts.map +1 -1
- package/dist/mixins/parent.js +6 -1
- package/dist/mixins/parent.js.map +1 -1
- package/dist/mixins/types.d.ts +2 -2
- package/dist/mixins/types.d.ts.map +1 -1
- package/dist/resources/references.d.ts +2 -0
- package/dist/resources/references.d.ts.map +1 -1
- package/dist/resources/references.js +3 -3
- package/dist/resources/references.js.map +1 -1
- package/dist/services/catalog.d.ts.map +1 -1
- package/dist/services/catalog.js +5 -3
- package/dist/services/catalog.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/defs-manager.d.ts +2 -0
- package/dist/services/defs-manager.d.ts.map +1 -1
- package/dist/services/defs-manager.js +4 -0
- package/dist/services/defs-manager.js.map +1 -1
- package/dist/services/element-key.d.ts +4 -0
- package/dist/services/element-key.d.ts.map +1 -0
- package/dist/services/element-key.js +12 -0
- package/dist/services/element-key.js.map +1 -0
- package/dist/services/initialization.d.ts +7 -0
- package/dist/services/initialization.d.ts.map +1 -0
- package/dist/services/initialization.js +27 -0
- package/dist/services/initialization.js.map +1 -0
- package/dist/services/registry.d.ts +8 -0
- package/dist/services/registry.d.ts.map +1 -1
- package/dist/services/registry.js +47 -10
- package/dist/services/registry.js.map +1 -1
- package/dist/services/types.d.ts +17 -0
- package/dist/services/types.d.ts.map +1 -1
- package/dist/template/parts.d.ts +12 -6
- package/dist/template/parts.d.ts.map +1 -1
- package/dist/template/parts.js +36 -29
- package/dist/template/parts.js.map +1 -1
- package/dist/template/render.d.ts +6 -7
- package/dist/template/render.d.ts.map +1 -1
- package/dist/template/render.js +188 -44
- package/dist/template/render.js.map +1 -1
- package/dist/template/svg-tag.d.ts +5 -1
- package/dist/template/svg-tag.d.ts.map +1 -1
- package/dist/template/svg-tag.js.map +1 -1
- package/dist/text/internals.d.ts +1 -0
- package/dist/text/internals.d.ts.map +1 -1
- package/dist/types/common.d.ts +27 -0
- package/dist/types/common.d.ts.map +1 -1
- package/package.json +10 -1
- package/reports/adoption.json +20 -20
- package/reports/animation-benchmark.json +613 -613
- package/reports/benchmark.json +27 -27
- package/reports/change-groups-benchmark.json +471 -0
- package/reports/components-benchmark-before.json +10 -0
- package/reports/components-benchmark.json +10 -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 +335 -17
- package/reports/size-components.json +103 -0
- package/reports/size-parity.json +50 -34
- package/reports/size.json +7 -4
- package/reports/svgjs-comparison.json +3049 -3049
- package/reports/svgjs-comparison.md +15 -15
- package/reports/svgjs-parity-current.json +133 -46
- package/src/advanced.ts +1 -1
- package/src/catalog/fragment.ts +2 -2
- package/src/changes/derived.ts +19 -0
- package/src/changes/restore.ts +78 -36
- package/src/changes/service.ts +23 -8
- package/src/changes/state.ts +37 -28
- package/src/changes.ts +24 -5
- package/src/components/changes.ts +131 -0
- package/src/components/component.ts +401 -0
- package/src/components/decorators.ts +106 -0
- package/src/components/metadata.ts +111 -0
- package/src/components/registry.ts +79 -0
- package/src/components/types.ts +66 -0
- package/src/components/view.ts +123 -0
- package/src/components.ts +15 -0
- package/src/core/node.ts +32 -1
- package/src/interchange/clone.ts +3 -2
- package/src/interchange/reconstruct.ts +16 -12
- package/src/interchange/references.ts +12 -4
- package/src/mixins/parent-internals.ts +37 -0
- package/src/mixins/parent.ts +13 -4
- package/src/mixins/types.ts +2 -2
- package/src/resources/references.ts +5 -5
- package/src/services/catalog.ts +5 -3
- package/src/services/claims.ts +14 -0
- package/src/services/defs-manager.ts +5 -0
- package/src/services/element-key.ts +14 -0
- package/src/services/initialization.ts +25 -0
- package/src/services/registry.ts +49 -10
- package/src/services/types.ts +21 -0
- package/src/template/parts.ts +54 -39
- package/src/template/render.ts +211 -54
- package/src/template/svg-tag.ts +6 -1
- package/src/text/internals.ts +1 -0
- package/src/types/common.ts +29 -0
package/src/changes/restore.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { AnyNode } from '../core/types.js';
|
|
2
2
|
import type { ParentNode } from '../mixins/types.js';
|
|
3
3
|
import { destroySubtrees } from '../core/destruction.js';
|
|
4
|
-
import { validateChild } from '../mixins/parent-internals.js';
|
|
4
|
+
import { validateChild, withInternalStructure } from '../mixins/parent-internals.js';
|
|
5
5
|
import { readTransformState } from '../mixins/transform-state.js';
|
|
6
6
|
import { referenceValue } from '../services/reference-value.js';
|
|
7
|
+
import { initializeNode } from '../services/initialization.js';
|
|
8
|
+
import { isDerivedChange, ownsDerived } from './derived.js';
|
|
7
9
|
import { captureState, sameState, stateAdapter, combineChanges } from './state.js';
|
|
8
10
|
import {
|
|
9
11
|
ChangeConflictError,
|
|
@@ -28,6 +30,14 @@ function currentValue(snapshot: ModelSnapshot | null, change: ModelChange): unkn
|
|
|
28
30
|
return snapshot[change.domain];
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** An owner's derived child list is redrawn, not restored, so conflicts ignore it. */
|
|
34
|
+
function comparable(value: unknown, change: ModelChange): unknown {
|
|
35
|
+
const snapshot = value as ModelSnapshot | null;
|
|
36
|
+
return change.domain === 'existence' && snapshot && ownsDerived(snapshot.constructor)
|
|
37
|
+
? { ...snapshot, children: undefined }
|
|
38
|
+
: value;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
function contentFeatures(content: ContentState): ModelFeatures['content'] {
|
|
32
42
|
return {
|
|
33
43
|
parts: content.parts.map((part) => (typeof part === 'string' ? part : part.node)),
|
|
@@ -58,8 +68,8 @@ function applyStateChange(
|
|
|
58
68
|
},
|
|
59
69
|
};
|
|
60
70
|
}
|
|
61
|
-
|
|
62
|
-
return { ...snapshot, [
|
|
71
|
+
const field = change.domain === 'bounds' ? 'contributesToBounds' : change.domain;
|
|
72
|
+
return { ...snapshot, [field]: value };
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
/** Validates the whole patch before writing; rolls back logical state on failure. */
|
|
@@ -80,7 +90,8 @@ function applyChanges(
|
|
|
80
90
|
direction: 'undo' | 'redo',
|
|
81
91
|
): void {
|
|
82
92
|
const service = owner.context.changes;
|
|
83
|
-
|
|
93
|
+
// An owner redraws its derived children after replay, from the state replay restored.
|
|
94
|
+
changes = combineChanges(changes).filter((change) => !isDerivedChange(change));
|
|
84
95
|
service.resetOrders();
|
|
85
96
|
service.configure(stateAdapter);
|
|
86
97
|
try {
|
|
@@ -97,7 +108,8 @@ function applyChanges(
|
|
|
97
108
|
domains.set(change.target, new Set());
|
|
98
109
|
}
|
|
99
110
|
const expected = direction === 'undo' ? change.after : change.before;
|
|
100
|
-
|
|
111
|
+
const current = currentValue(before.get(change.target)!, change);
|
|
112
|
+
if (!sameState(comparable(current, change), comparable(expected, change)))
|
|
101
113
|
throw new ChangeConflictError(
|
|
102
114
|
`Conflicting ${change.domain} state on node ${change.target.id}`,
|
|
103
115
|
);
|
|
@@ -174,18 +186,37 @@ function validateGraph(
|
|
|
174
186
|
): void {
|
|
175
187
|
const context = owner.context;
|
|
176
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
|
+
}
|
|
177
203
|
for (const [target, state] of states) {
|
|
178
204
|
if (!state) continue;
|
|
179
|
-
if (context.registry.
|
|
205
|
+
if (context.registry.registered(state.constructor) !== state.constructor)
|
|
180
206
|
throw new ChangeConflictError(`Constructor is not registered: ${state.constructor.tag}`);
|
|
181
207
|
const id = state.properties.id?.value;
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
}
|
|
189
220
|
if (!target.node.isDestroyed) {
|
|
190
221
|
for (const [attribute, property] of Object.entries(state.properties))
|
|
191
222
|
target.node.validateAttribute(attribute, property.value);
|
|
@@ -209,22 +240,10 @@ function validateGraph(
|
|
|
209
240
|
throw new ChangeConflictError('The reference value and its planned target ID disagree');
|
|
210
241
|
}
|
|
211
242
|
if (state.placement.parent) {
|
|
212
|
-
const parent = state.placement.parent;
|
|
213
|
-
const existing =
|
|
214
|
-
!parent.node.isDestroyed && 'children' in parent.node
|
|
215
|
-
? (parent.node as ParentNode).children.map((node) => context.changes.handle(node))
|
|
216
|
-
: [];
|
|
217
|
-
const children = new Set(
|
|
218
|
-
existing.filter(
|
|
219
|
-
(child) => !states.has(child) || states.get(child)?.placement.parent === parent,
|
|
220
|
-
),
|
|
221
|
-
);
|
|
222
|
-
for (const [child, planned] of states)
|
|
223
|
-
if (planned?.placement.parent === parent) children.add(child);
|
|
224
243
|
if (
|
|
225
244
|
!Number.isInteger(state.placement.index) ||
|
|
226
245
|
state.placement.index < 0 ||
|
|
227
|
-
state.placement.index >=
|
|
246
|
+
state.placement.index >= siblingCounts.get(state.placement.parent)!
|
|
228
247
|
)
|
|
229
248
|
throw new ChangeConflictError('The history sibling position is no longer available');
|
|
230
249
|
}
|
|
@@ -258,10 +277,19 @@ function validateGraph(
|
|
|
258
277
|
throw new ChangeConflictError('A removed node would retain live typed references');
|
|
259
278
|
}
|
|
260
279
|
|
|
280
|
+
/** Replay restores exact prior trees, including children that their class owns. */
|
|
261
281
|
function applyPrepared(
|
|
262
282
|
owner: AnyNode,
|
|
263
283
|
states: Map<NodeHandle, ModelSnapshot | null>,
|
|
264
284
|
domains: Map<NodeHandle, Set<string>>,
|
|
285
|
+
): void {
|
|
286
|
+
withInternalStructure(() => applyStates(owner, states, domains));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function applyStates(
|
|
290
|
+
owner: AnyNode,
|
|
291
|
+
states: Map<NodeHandle, ModelSnapshot | null>,
|
|
292
|
+
domains: Map<NodeHandle, Set<string>>,
|
|
265
293
|
): void {
|
|
266
294
|
const context = owner.context;
|
|
267
295
|
const recreated = new Set<NodeHandle>();
|
|
@@ -321,28 +349,41 @@ function applyPrepared(
|
|
|
321
349
|
domains.get(target)!.has('existence')),
|
|
322
350
|
);
|
|
323
351
|
for (const [target] of placements) if (target.node.parent) target.node.remove();
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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])!,
|
|
329
370
|
);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
for (const entry of ready) {
|
|
333
|
-
const [target, state] = entry;
|
|
371
|
+
const next: typeof placements = [];
|
|
372
|
+
for (const [target, state] of ready) {
|
|
334
373
|
const parent = state!.placement.parent?.node as ParentNode | undefined;
|
|
335
374
|
if (parent) {
|
|
336
375
|
parent[validateChild](target.node);
|
|
337
376
|
parent.add(target.node, Math.min(state!.placement.index, parent.children.length));
|
|
338
377
|
}
|
|
339
|
-
|
|
378
|
+
for (const child of dependants.get(target) ?? []) next.push(child);
|
|
340
379
|
}
|
|
380
|
+
ready = next;
|
|
341
381
|
}
|
|
342
382
|
// Parent order is authoritative across compound edits and separate edit callbacks.
|
|
343
383
|
for (const [target, state] of states)
|
|
344
384
|
if (
|
|
345
385
|
state?.children &&
|
|
386
|
+
!ownsDerived(state.constructor) &&
|
|
346
387
|
(recreated.has(target) ||
|
|
347
388
|
domains.get(target)!.has('children') ||
|
|
348
389
|
domains.get(target)!.has('existence'))
|
|
@@ -371,5 +412,6 @@ function applyPrepared(
|
|
|
371
412
|
);
|
|
372
413
|
const roots = [...retired].filter((node) => !node.parent || !retired.has(node.parent));
|
|
373
414
|
if (roots.length) owner[destroySubtrees](roots);
|
|
415
|
+
for (const target of recreated) initializeNode(target.node);
|
|
374
416
|
context.bounds.invalidate();
|
|
375
417
|
}
|
package/src/changes/service.ts
CHANGED
|
@@ -16,6 +16,10 @@ interface ChangeAdapter {
|
|
|
16
16
|
diff(target: NodeHandle, before: ModelSnapshot, after: ModelSnapshot): ModelChange[];
|
|
17
17
|
immutable<Value>(value: Value): Value;
|
|
18
18
|
}
|
|
19
|
+
interface ChildOrder {
|
|
20
|
+
readonly nodes: readonly AnyNode[];
|
|
21
|
+
readonly indices: ReadonlyMap<AnyNode, number>;
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
export class ChangeService {
|
|
21
25
|
private readonly listeners = new Set<(event: ChangeEvent) => void>();
|
|
@@ -41,10 +45,8 @@ export class ChangeService {
|
|
|
41
45
|
}[] = [];
|
|
42
46
|
private adapter?: ChangeAdapter;
|
|
43
47
|
private validationCounts?: ReadonlyMap<AnyNode, number>;
|
|
44
|
-
private childOrders = new Map<
|
|
45
|
-
|
|
46
|
-
{ nodes: readonly AnyNode[]; indices: ReadonlyMap<AnyNode, number> }
|
|
47
|
-
>();
|
|
48
|
+
private childOrders = new Map<AnyNode, ChildOrder>();
|
|
49
|
+
private liveOrders = new WeakMap<AnyNode, ChildOrder>();
|
|
48
50
|
|
|
49
51
|
constructor(private readonly context: DrawingContext) {}
|
|
50
52
|
|
|
@@ -54,13 +56,21 @@ export class ChangeService {
|
|
|
54
56
|
get replaying(): boolean {
|
|
55
57
|
return this.replayDepth > 0;
|
|
56
58
|
}
|
|
59
|
+
/** The surrounding synchronous edit, if one is already grouping model writes. */
|
|
60
|
+
get currentGroup(): number | undefined {
|
|
61
|
+
return this.metadata.group;
|
|
62
|
+
}
|
|
57
63
|
|
|
58
64
|
children(node: AnyNode): readonly AnyNode[] | undefined {
|
|
59
65
|
if (!('children' in node)) return undefined;
|
|
60
66
|
let order = this.childOrders.get(node);
|
|
61
67
|
if (!order) {
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
order = this.liveOrders.get(node);
|
|
69
|
+
if (!order) {
|
|
70
|
+
const nodes = [...(node as AnyNode & { children: readonly AnyNode[] }).children];
|
|
71
|
+
order = { nodes, indices: new Map(nodes.map((child, index) => [child, index])) };
|
|
72
|
+
this.liveOrders.set(node, order);
|
|
73
|
+
}
|
|
64
74
|
this.childOrders.set(node, order);
|
|
65
75
|
}
|
|
66
76
|
return order.nodes;
|
|
@@ -75,6 +85,10 @@ export class ChangeService {
|
|
|
75
85
|
resetOrders(): void {
|
|
76
86
|
this.childOrders = new Map();
|
|
77
87
|
}
|
|
88
|
+
/** Keep the operation's original order, but invalidate the live order after a tree edit. */
|
|
89
|
+
invalidateOrder(node: AnyNode): void {
|
|
90
|
+
this.liveOrders.delete(node);
|
|
91
|
+
}
|
|
78
92
|
|
|
79
93
|
reading<Result>(callback: () => Result): Result {
|
|
80
94
|
const previous = this.capturing;
|
|
@@ -391,8 +405,7 @@ export class ChangeService {
|
|
|
391
405
|
this.delivering = true;
|
|
392
406
|
const failures: unknown[] = [];
|
|
393
407
|
try {
|
|
394
|
-
|
|
395
|
-
const next = this.queue.shift()!;
|
|
408
|
+
for (const next of this.queue) {
|
|
396
409
|
for (const listener of [...this.listeners]) {
|
|
397
410
|
if (!this.listeners.has(listener)) continue;
|
|
398
411
|
try {
|
|
@@ -403,6 +416,7 @@ export class ChangeService {
|
|
|
403
416
|
}
|
|
404
417
|
}
|
|
405
418
|
} finally {
|
|
419
|
+
this.queue.length = 0;
|
|
406
420
|
this.delivering = false;
|
|
407
421
|
}
|
|
408
422
|
if (failures.length === 1 && failures[0] instanceof ChangeConflictError) throw failures[0];
|
|
@@ -418,5 +432,6 @@ export class ChangeService {
|
|
|
418
432
|
this.groups.clear();
|
|
419
433
|
this.owner = null;
|
|
420
434
|
this.childOrders.clear();
|
|
435
|
+
this.liveOrders = new WeakMap();
|
|
421
436
|
}
|
|
422
437
|
}
|
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;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { AnyNode } from '../core/types.js';
|
|
2
|
+
import type { DrawingContext } from '../services/context.js';
|
|
3
|
+
import { deriveChanges, observeChanges, withChanges, type ChangeBatch } from '../changes.js';
|
|
4
|
+
|
|
5
|
+
export interface Binding {
|
|
6
|
+
readonly node: AnyNode;
|
|
7
|
+
derive(): void;
|
|
8
|
+
replay(): void;
|
|
9
|
+
paint(): void;
|
|
10
|
+
}
|
|
11
|
+
interface Discovery {
|
|
12
|
+
added(node: AnyNode): void;
|
|
13
|
+
identityChanged(node: AnyNode): void;
|
|
14
|
+
}
|
|
15
|
+
const drawings = new WeakMap<DrawingContext, ComponentChanges>();
|
|
16
|
+
|
|
17
|
+
/** Routes work by changed node, independent of the number of components in a drawing. */
|
|
18
|
+
export class ComponentChanges {
|
|
19
|
+
private readonly bindings = new WeakMap<AnyNode, Binding>();
|
|
20
|
+
private readonly discovery = new Set<Discovery>();
|
|
21
|
+
private bindingCount = 0;
|
|
22
|
+
private readonly dirty = new Set<Binding>();
|
|
23
|
+
private cancelFrame?: () => void;
|
|
24
|
+
private readonly stopDerive: () => void;
|
|
25
|
+
private readonly stopObserve: () => void;
|
|
26
|
+
private readonly context: DrawingContext;
|
|
27
|
+
|
|
28
|
+
static for(node: AnyNode): ComponentChanges {
|
|
29
|
+
let changes = drawings.get(node.context);
|
|
30
|
+
if (!changes) {
|
|
31
|
+
changes = new ComponentChanges(node);
|
|
32
|
+
drawings.set(node.context, changes);
|
|
33
|
+
}
|
|
34
|
+
return changes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private constructor(drawing: AnyNode) {
|
|
38
|
+
this.context = drawing.context;
|
|
39
|
+
this.stopDerive = deriveChanges(drawing, ({ nodes }) => {
|
|
40
|
+
for (const node of nodes) this.bindings.get(node)?.derive();
|
|
41
|
+
});
|
|
42
|
+
this.stopObserve = observeChanges(drawing, (event) => {
|
|
43
|
+
if (event.type === 'change') this.dispatch(event);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
bind(binding: Binding): () => void {
|
|
48
|
+
if (this.bindings.has(binding.node)) throw new Error('Component is already observing changes');
|
|
49
|
+
this.bindings.set(binding.node, binding);
|
|
50
|
+
this.bindingCount++;
|
|
51
|
+
return this.release(() => {
|
|
52
|
+
this.bindings.delete(binding.node);
|
|
53
|
+
this.dirty.delete(binding);
|
|
54
|
+
this.bindingCount--;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
discover(discovery: Discovery): () => void {
|
|
59
|
+
this.discovery.add(discovery);
|
|
60
|
+
return this.release(() => this.discovery.delete(discovery));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private release(cleanup: () => void): () => void {
|
|
64
|
+
let active = true;
|
|
65
|
+
return () => {
|
|
66
|
+
if (!active) return;
|
|
67
|
+
active = false;
|
|
68
|
+
cleanup();
|
|
69
|
+
if (!this.bindingCount && !this.discovery.size) {
|
|
70
|
+
this.cancelFrame?.();
|
|
71
|
+
this.stopDerive();
|
|
72
|
+
this.stopObserve();
|
|
73
|
+
drawings.delete(this.context);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Live state from every instance renders in one shared frame, as one temporary change. */
|
|
79
|
+
schedule(binding: Binding): void {
|
|
80
|
+
this.dirty.add(binding);
|
|
81
|
+
this.cancelFrame ??= this.context.dom.requestFrame(() => {
|
|
82
|
+
this.cancelFrame = undefined;
|
|
83
|
+
this.paint(this.dirty);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Renders without an undo step; failures surface after every other instance rendered. */
|
|
88
|
+
paint(bindings: Iterable<Binding>): void {
|
|
89
|
+
const list = [...bindings].filter((binding) => !binding.node.isDestroyed);
|
|
90
|
+
for (const binding of list) this.dirty.delete(binding);
|
|
91
|
+
if (!list.length) return;
|
|
92
|
+
const failures: unknown[] = [];
|
|
93
|
+
withChanges(list[0].node, { source: 'components', temporary: true }, () => {
|
|
94
|
+
for (const binding of list)
|
|
95
|
+
try {
|
|
96
|
+
binding.paint();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
failures.push(error);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
if (failures.length > 1) throw new AggregateError(failures, 'Component renders failed');
|
|
102
|
+
if (failures.length) throw failures[0];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private dispatch(event: ChangeBatch): void {
|
|
106
|
+
// Replay skips rendered children, so restored and recreated instances redraw together.
|
|
107
|
+
const replayed = new Set<Binding>();
|
|
108
|
+
for (const change of event.changes) {
|
|
109
|
+
const node = change.target.node,
|
|
110
|
+
binding = this.bindings.get(node);
|
|
111
|
+
if (
|
|
112
|
+
binding &&
|
|
113
|
+
event.source === 'replay' &&
|
|
114
|
+
((change.domain === 'property' && change.attribute === 'data-rasam-props') ||
|
|
115
|
+
(change.domain === 'existence' && change.after))
|
|
116
|
+
)
|
|
117
|
+
replayed.add(binding);
|
|
118
|
+
if (!this.discovery.size) continue;
|
|
119
|
+
if (
|
|
120
|
+
(change.domain === 'placement' && change.after.parent && !change.before.parent) ||
|
|
121
|
+
(change.domain === 'existence' && change.after)
|
|
122
|
+
)
|
|
123
|
+
for (const subscriber of this.discovery) subscriber.added(node);
|
|
124
|
+
else if (change.domain === 'property' && change.attribute === 'data-rasam-component')
|
|
125
|
+
for (const subscriber of this.discovery) subscriber.identityChanged(node);
|
|
126
|
+
}
|
|
127
|
+
if (!replayed.size) return;
|
|
128
|
+
for (const binding of replayed) binding.replay();
|
|
129
|
+
this.paint(replayed);
|
|
130
|
+
}
|
|
131
|
+
}
|