@rasamjs/core 0.1.2 → 0.3.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/API.md +29 -9
- package/CHANGELOG.md +98 -1
- package/COMPONENTS.md +229 -0
- package/README.md +178 -25
- package/RELEASING.md +31 -21
- 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 +22 -4
- package/dist/changes/restore.js.map +1 -1
- package/dist/changes/service.d.ts +10 -2
- package/dist/changes/service.d.ts.map +1 -1
- package/dist/changes/service.js +45 -6
- package/dist/changes/service.js.map +1 -1
- package/dist/changes/types.d.ts +9 -0
- package/dist/changes/types.d.ts.map +1 -1
- package/dist/changes/types.js.map +1 -1
- package/dist/changes.d.ts +5 -3
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +103 -2
- 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 +81 -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/dom/images.d.ts.map +1 -1
- package/dist/dom/images.js +3 -1
- package/dist/dom/images.js.map +1 -1
- package/dist/elements/image.d.ts.map +1 -1
- package/dist/elements/image.js +4 -1
- package/dist/elements/image.js.map +1 -1
- package/dist/interchange/clone.d.ts.map +1 -1
- package/dist/interchange/clone.js +4 -3
- package/dist/interchange/clone.js.map +1 -1
- package/dist/interchange/dom.d.ts +3 -1
- package/dist/interchange/dom.d.ts.map +1 -1
- package/dist/interchange/dom.js +29 -5
- package/dist/interchange/dom.js.map +1 -1
- package/dist/interchange/markup.d.ts.map +1 -1
- package/dist/interchange/markup.js +71 -3
- package/dist/interchange/markup.js.map +1 -1
- package/dist/interchange/policy.d.ts.map +1 -1
- package/dist/interchange/policy.js +4 -0
- package/dist/interchange/policy.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/interchange/types.d.ts +10 -1
- package/dist/interchange/types.d.ts.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/transformable.d.ts.map +1 -1
- package/dist/mixins/transformable.js +25 -27
- package/dist/mixins/transformable.js.map +1 -1
- package/dist/mixins/types.d.ts +2 -2
- package/dist/mixins/types.d.ts.map +1 -1
- package/dist/raster/frame.d.ts +12 -0
- package/dist/raster/frame.d.ts.map +1 -0
- package/dist/raster/frame.js +43 -0
- package/dist/raster/frame.js.map +1 -0
- package/dist/raster/images.d.ts +2 -0
- package/dist/raster/images.d.ts.map +1 -0
- package/dist/raster/images.js +60 -0
- package/dist/raster/images.js.map +1 -0
- package/dist/raster/render.d.ts +3 -0
- package/dist/raster/render.d.ts.map +1 -0
- package/dist/raster/render.js +40 -0
- package/dist/raster/render.js.map +1 -0
- package/dist/raster/types.d.ts +10 -0
- package/dist/raster/types.d.ts.map +1 -0
- package/dist/raster/types.js +2 -0
- package/dist/raster/types.js.map +1 -0
- package/dist/raster.d.ts +6 -0
- package/dist/raster.d.ts.map +1 -0
- package/dist/raster.js +10 -0
- package/dist/raster.js.map +1 -0
- package/dist/resources/references.d.ts +2 -0
- package/dist/resources/references.d.ts.map +1 -1
- package/dist/resources/references.js +3 -1
- 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/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 +45 -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/services/viewport.d.ts +2 -0
- package/dist/services/viewport.d.ts.map +1 -1
- package/dist/services/viewport.js +4 -3
- package/dist/services/viewport.js.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/content.d.ts +0 -2
- package/dist/text/content.d.ts.map +1 -1
- package/dist/text/content.js +4 -10
- package/dist/text/content.js.map +1 -1
- package/dist/text/internals.d.ts +1 -0
- package/dist/text/internals.d.ts.map +1 -1
- package/dist/text/policies.d.ts +7 -0
- package/dist/text/policies.d.ts.map +1 -0
- package/dist/text/policies.js +13 -0
- package/dist/text/policies.js.map +1 -0
- package/dist/types/common.d.ts +27 -0
- package/dist/types/common.d.ts.map +1 -1
- package/package.json +156 -143
- package/reports/adoption.json +20 -20
- package/reports/animation-benchmark.json +616 -616
- package/reports/benchmark.json +65 -65
- 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/public-api-contract.json +353 -23
- package/reports/public-api-review.md +23 -0
- package/reports/size-components.json +102 -0
- package/reports/size-parity.json +82 -34
- package/reports/size.json +6 -4
- package/reports/svgjs-comparison.json +3466 -3466
- package/reports/svgjs-comparison.md +19 -19
- package/reports/svgjs-parity-current.json +177 -60
- 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 +27 -4
- package/src/changes/service.ts +60 -11
- package/src/changes/types.ts +9 -0
- package/src/changes.ts +93 -4
- package/src/components/changes.ts +131 -0
- package/src/components/component.ts +401 -0
- package/src/components/decorators.ts +104 -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/dom/images.ts +2 -1
- package/src/elements/image.ts +4 -1
- package/src/interchange/clone.ts +4 -3
- package/src/interchange/dom.ts +34 -5
- package/src/interchange/markup.ts +77 -3
- package/src/interchange/policy.ts +6 -0
- package/src/interchange/reconstruct.ts +16 -12
- package/src/interchange/references.ts +12 -4
- package/src/interchange/types.ts +10 -1
- package/src/mixins/parent-internals.ts +37 -0
- package/src/mixins/parent.ts +13 -4
- package/src/mixins/transformable.ts +23 -25
- package/src/mixins/types.ts +2 -2
- package/src/raster/frame.ts +53 -0
- package/src/raster/images.ts +73 -0
- package/src/raster/render.ts +45 -0
- package/src/raster/types.ts +9 -0
- package/src/raster.ts +13 -0
- package/src/resources/references.ts +5 -1
- package/src/services/catalog.ts +5 -3
- 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 +47 -10
- package/src/services/types.ts +21 -0
- package/src/services/viewport.ts +5 -4
- 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/content.ts +4 -14
- package/src/text/internals.ts +1 -0
- package/src/text/policies.ts +18 -0
- package/src/types/common.ts +29 -0
package/src/advanced.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { Namespace } from './dom/namespace.js';
|
|
|
11
11
|
export { DrawingContext } from './services/context.js';
|
|
12
12
|
export { Scheduler } from './services/scheduler.js';
|
|
13
13
|
export { Registry } from './services/registry.js';
|
|
14
|
-
export type { ElementClass } from './services/types.js';
|
|
14
|
+
export type { ElementClass, ElementVariant } from './services/types.js';
|
|
15
15
|
export { BoundsService } from './services/bounds.js';
|
|
16
16
|
export { DefsManager } from './services/defs-manager.js';
|
|
17
17
|
|
package/src/catalog/fragment.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Group } from '../elements/group.js';
|
|
2
2
|
import type { AnyNode } from '../core/types.js';
|
|
3
|
-
import type { ElementClass } from '../services/types.js';
|
|
3
|
+
import type { ElementClass, CreationOptions } from '../services/types.js';
|
|
4
4
|
import type { ParentNode } from '../mixins/types.js';
|
|
5
5
|
import { validateChild, validateIndex } from '../mixins/parent-internals.js';
|
|
6
6
|
import { destroySubtrees } from '../core/destruction.js';
|
|
@@ -32,7 +32,7 @@ export class SvgFragment {
|
|
|
32
32
|
}
|
|
33
33
|
create<Options, Child extends AnyNode>(
|
|
34
34
|
Constructor: ElementClass<Options, Child>,
|
|
35
|
-
options: Options,
|
|
35
|
+
options: NoInfer<CreationOptions<Options, Child>>,
|
|
36
36
|
): Child {
|
|
37
37
|
return this.#host.create(Constructor, options);
|
|
38
38
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AnyNode } from '../core/types.js';
|
|
2
|
+
import type { ModelChange } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** Marks a class whose children are drawn from its own state, such as a component. */
|
|
5
|
+
export const derivesChildren = Symbol('derivesChildren');
|
|
6
|
+
const derived = new WeakSet<AnyNode>();
|
|
7
|
+
|
|
8
|
+
export const ownsDerived = (Constructor: unknown): boolean =>
|
|
9
|
+
!!(Constructor as { [derivesChildren]?: boolean } | undefined)?.[derivesChildren];
|
|
10
|
+
|
|
11
|
+
/** Records the owner's current descendants as derived. The owner redraws them after replay. */
|
|
12
|
+
export function markDerived(owner: AnyNode): void {
|
|
13
|
+
for (const node of owner.descendants()) if (node !== owner) derived.add(node);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Replay skips derived nodes and their owners' child lists. */
|
|
17
|
+
export const isDerivedChange = (change: ModelChange): boolean =>
|
|
18
|
+
derived.has(change.target.node) ||
|
|
19
|
+
(change.domain === 'children' && ownsDerived(change.target.node.constructor));
|
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)),
|
|
@@ -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
|
);
|
|
@@ -176,7 +188,7 @@ function validateGraph(
|
|
|
176
188
|
const removed = new Set([...states].filter(([, state]) => !state).map(([target]) => target.node));
|
|
177
189
|
for (const [target, state] of states) {
|
|
178
190
|
if (!state) continue;
|
|
179
|
-
if (context.registry.
|
|
191
|
+
if (context.registry.registered(state.constructor) !== state.constructor)
|
|
180
192
|
throw new ChangeConflictError(`Constructor is not registered: ${state.constructor.tag}`);
|
|
181
193
|
const id = state.properties.id?.value;
|
|
182
194
|
if (
|
|
@@ -258,10 +270,19 @@ function validateGraph(
|
|
|
258
270
|
throw new ChangeConflictError('A removed node would retain live typed references');
|
|
259
271
|
}
|
|
260
272
|
|
|
273
|
+
/** Replay restores exact prior trees, including children that their class owns. */
|
|
261
274
|
function applyPrepared(
|
|
262
275
|
owner: AnyNode,
|
|
263
276
|
states: Map<NodeHandle, ModelSnapshot | null>,
|
|
264
277
|
domains: Map<NodeHandle, Set<string>>,
|
|
278
|
+
): void {
|
|
279
|
+
withInternalStructure(() => applyStates(owner, states, domains));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function applyStates(
|
|
283
|
+
owner: AnyNode,
|
|
284
|
+
states: Map<NodeHandle, ModelSnapshot | null>,
|
|
285
|
+
domains: Map<NodeHandle, Set<string>>,
|
|
265
286
|
): void {
|
|
266
287
|
const context = owner.context;
|
|
267
288
|
const recreated = new Set<NodeHandle>();
|
|
@@ -343,6 +364,7 @@ function applyPrepared(
|
|
|
343
364
|
for (const [target, state] of states)
|
|
344
365
|
if (
|
|
345
366
|
state?.children &&
|
|
367
|
+
!ownsDerived(state.constructor) &&
|
|
346
368
|
(recreated.has(target) ||
|
|
347
369
|
domains.get(target)!.has('children') ||
|
|
348
370
|
domains.get(target)!.has('existence'))
|
|
@@ -371,5 +393,6 @@ function applyPrepared(
|
|
|
371
393
|
);
|
|
372
394
|
const roots = [...retired].filter((node) => !node.parent || !retired.has(node.parent));
|
|
373
395
|
if (roots.length) owner[destroySubtrees](roots);
|
|
396
|
+
for (const target of recreated) initializeNode(target.node);
|
|
374
397
|
context.bounds.invalidate();
|
|
375
398
|
}
|
package/src/changes/service.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ChangeEvent,
|
|
5
5
|
ChangeGroup,
|
|
6
6
|
ChangeMetadata,
|
|
7
|
+
ChangeDerivation,
|
|
7
8
|
ModelChange,
|
|
8
9
|
ModelSnapshot,
|
|
9
10
|
NodeHandle,
|
|
@@ -15,9 +16,14 @@ interface ChangeAdapter {
|
|
|
15
16
|
diff(target: NodeHandle, before: ModelSnapshot, after: ModelSnapshot): ModelChange[];
|
|
16
17
|
immutable<Value>(value: Value): Value;
|
|
17
18
|
}
|
|
19
|
+
interface ChildOrder {
|
|
20
|
+
readonly nodes: readonly AnyNode[];
|
|
21
|
+
readonly indices: ReadonlyMap<AnyNode, number>;
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
export class ChangeService {
|
|
20
25
|
private readonly listeners = new Set<(event: ChangeEvent) => void>();
|
|
26
|
+
readonly derivations = new Set<(event: ChangeDerivation) => void>();
|
|
21
27
|
private readonly identities = new WeakMap<AnyNode, NodeHandle>();
|
|
22
28
|
private readonly retained = new WeakSet<AnyNode>();
|
|
23
29
|
private readonly queue: ChangeEvent[] = [];
|
|
@@ -39,26 +45,32 @@ export class ChangeService {
|
|
|
39
45
|
}[] = [];
|
|
40
46
|
private adapter?: ChangeAdapter;
|
|
41
47
|
private validationCounts?: ReadonlyMap<AnyNode, number>;
|
|
42
|
-
private childOrders = new Map<
|
|
43
|
-
|
|
44
|
-
{ nodes: readonly AnyNode[]; indices: ReadonlyMap<AnyNode, number> }
|
|
45
|
-
>();
|
|
48
|
+
private childOrders = new Map<AnyNode, ChildOrder>();
|
|
49
|
+
private liveOrders = new WeakMap<AnyNode, ChildOrder>();
|
|
46
50
|
|
|
47
51
|
constructor(private readonly context: DrawingContext) {}
|
|
48
52
|
|
|
49
53
|
get observed(): boolean {
|
|
50
|
-
return this.listeners.size > 0;
|
|
54
|
+
return this.listeners.size > 0 || this.derivations.size > 0;
|
|
51
55
|
}
|
|
52
56
|
get replaying(): boolean {
|
|
53
57
|
return this.replayDepth > 0;
|
|
54
58
|
}
|
|
59
|
+
/** The surrounding synchronous edit, if one is already grouping model writes. */
|
|
60
|
+
get currentGroup(): number | undefined {
|
|
61
|
+
return this.metadata.group;
|
|
62
|
+
}
|
|
55
63
|
|
|
56
64
|
children(node: AnyNode): readonly AnyNode[] | undefined {
|
|
57
65
|
if (!('children' in node)) return undefined;
|
|
58
66
|
let order = this.childOrders.get(node);
|
|
59
67
|
if (!order) {
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
|
62
74
|
this.childOrders.set(node, order);
|
|
63
75
|
}
|
|
64
76
|
return order.nodes;
|
|
@@ -73,6 +85,10 @@ export class ChangeService {
|
|
|
73
85
|
resetOrders(): void {
|
|
74
86
|
this.childOrders = new Map();
|
|
75
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
|
+
}
|
|
76
92
|
|
|
77
93
|
reading<Result>(callback: () => Result): Result {
|
|
78
94
|
const previous = this.capturing;
|
|
@@ -194,6 +210,20 @@ export class ChangeService {
|
|
|
194
210
|
try {
|
|
195
211
|
if (node) this.touch(node);
|
|
196
212
|
result = callback();
|
|
213
|
+
if (this.before.size && this.derivations.size) {
|
|
214
|
+
const nodes = [...this.before]
|
|
215
|
+
.filter(
|
|
216
|
+
([target, previous]) =>
|
|
217
|
+
target.node.isDestroyed ||
|
|
218
|
+
this.adapter!.diff(target, previous, this.adapter!.capture(target.node)).length,
|
|
219
|
+
)
|
|
220
|
+
.map(([target]) => target.node);
|
|
221
|
+
if (nodes.length) {
|
|
222
|
+
const event = { ...this.metadata, nodes };
|
|
223
|
+
for (const derive of [...this.derivations])
|
|
224
|
+
if (this.derivations.has(derive)) derive(event);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
197
227
|
} catch (error) {
|
|
198
228
|
failure = { error };
|
|
199
229
|
}
|
|
@@ -266,13 +296,22 @@ export class ChangeService {
|
|
|
266
296
|
}
|
|
267
297
|
}
|
|
268
298
|
|
|
269
|
-
group(metadata: ChangeMetadata = {}): ChangeGroup {
|
|
299
|
+
group(metadata: ChangeMetadata = {}): Omit<ChangeGroup, 'cancel'> & {
|
|
300
|
+
end(cancelled?: boolean, restored?: boolean): void;
|
|
301
|
+
} {
|
|
270
302
|
const id = ++this.nextGroup,
|
|
271
303
|
source = metadata.source ?? 'application';
|
|
272
304
|
let active = true;
|
|
273
305
|
this.groups.add(id);
|
|
274
306
|
try {
|
|
275
|
-
this.publish(
|
|
307
|
+
this.publish(
|
|
308
|
+
Object.freeze({
|
|
309
|
+
type: 'groupstart',
|
|
310
|
+
group: id,
|
|
311
|
+
source,
|
|
312
|
+
...(metadata.label === undefined ? {} : { label: metadata.label }),
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
276
315
|
} catch (error) {
|
|
277
316
|
this.groups.delete(id);
|
|
278
317
|
this.publish(Object.freeze({ type: 'groupend', group: id, source, cancelled: true }));
|
|
@@ -284,11 +323,19 @@ export class ChangeService {
|
|
|
284
323
|
if (!active) throw new Error('Change group has ended');
|
|
285
324
|
return this.scope({ ...metadata, group: id }, callback, null);
|
|
286
325
|
},
|
|
287
|
-
end: (cancelled = false) => {
|
|
326
|
+
end: (cancelled = false, restored = false) => {
|
|
288
327
|
if (!active) return;
|
|
289
328
|
active = false;
|
|
290
329
|
this.groups.delete(id);
|
|
291
|
-
this.publish(
|
|
330
|
+
this.publish(
|
|
331
|
+
Object.freeze({
|
|
332
|
+
type: 'groupend',
|
|
333
|
+
group: id,
|
|
334
|
+
source,
|
|
335
|
+
cancelled,
|
|
336
|
+
...(restored ? { restored: true } : {}),
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
292
339
|
},
|
|
293
340
|
});
|
|
294
341
|
}
|
|
@@ -378,11 +425,13 @@ export class ChangeService {
|
|
|
378
425
|
|
|
379
426
|
destroy(): void {
|
|
380
427
|
this.listeners.clear();
|
|
428
|
+
this.derivations.clear();
|
|
381
429
|
this.queue.length = 0;
|
|
382
430
|
this.notifications.length = 0;
|
|
383
431
|
this.replacements.length = 0;
|
|
384
432
|
this.groups.clear();
|
|
385
433
|
this.owner = null;
|
|
386
434
|
this.childOrders.clear();
|
|
435
|
+
this.liveOrders = new WeakMap();
|
|
387
436
|
}
|
|
388
437
|
}
|
package/src/changes/types.ts
CHANGED
|
@@ -104,9 +104,14 @@ export type ModelChange =
|
|
|
104
104
|
|
|
105
105
|
export interface ChangeMetadata {
|
|
106
106
|
readonly source?: string;
|
|
107
|
+
readonly label?: string;
|
|
107
108
|
readonly temporary?: boolean;
|
|
108
109
|
readonly group?: number;
|
|
109
110
|
}
|
|
111
|
+
/** Runs synchronously after an operation's writes, before its final change snapshot. */
|
|
112
|
+
export interface ChangeDerivation extends ChangeMetadata {
|
|
113
|
+
readonly nodes: readonly AnyNode[];
|
|
114
|
+
}
|
|
110
115
|
export interface ChangeBatch {
|
|
111
116
|
readonly type: 'change';
|
|
112
117
|
readonly id: number;
|
|
@@ -126,7 +131,9 @@ export type ChangeEvent =
|
|
|
126
131
|
readonly type: 'groupstart' | 'groupend';
|
|
127
132
|
readonly group: number;
|
|
128
133
|
readonly source: string;
|
|
134
|
+
readonly label?: string;
|
|
129
135
|
readonly cancelled?: boolean;
|
|
136
|
+
readonly restored?: boolean;
|
|
130
137
|
}
|
|
131
138
|
| {
|
|
132
139
|
readonly type: 'dispose' | 'willdispose';
|
|
@@ -136,6 +143,8 @@ export interface ChangeGroup {
|
|
|
136
143
|
readonly id: number;
|
|
137
144
|
run<Result>(callback: () => Result): Result;
|
|
138
145
|
end(cancelled?: boolean): void;
|
|
146
|
+
/** Restore this group's own non-temporary changes, then end it as cancelled. */
|
|
147
|
+
cancel(): void;
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
export class ChangeConflictError extends Error {
|
package/src/changes.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { AnyNode } from './core/types.js';
|
|
2
|
-
import type { ChangeEvent, ChangeMetadata } from './changes/types.js';
|
|
3
|
-
import {
|
|
2
|
+
import type { ChangeEvent, ChangeGroup, ChangeMetadata, ModelChange } from './changes/types.js';
|
|
3
|
+
import { ChangeConflictError, ChangeObserverError } from './changes/types.js';
|
|
4
|
+
import { combineChanges, stateAdapter } from './changes/state.js';
|
|
5
|
+
import { restoreChanges } from './changes/restore.js';
|
|
4
6
|
|
|
5
7
|
export type {
|
|
6
8
|
ChangeBatch,
|
|
7
9
|
ChangeEvent,
|
|
8
10
|
ChangeGroup,
|
|
9
11
|
ChangeMetadata,
|
|
12
|
+
ChangeDerivation,
|
|
10
13
|
ModelChange,
|
|
11
14
|
ModelSnapshot,
|
|
12
15
|
ModelFeatures,
|
|
@@ -35,6 +38,20 @@ export function observeChanges(
|
|
|
35
38
|
drawing.assertLive();
|
|
36
39
|
return drawing.context.changes.observe(listener, stateAdapter);
|
|
37
40
|
}
|
|
41
|
+
/** Derived writes join the same operation and group. Callbacks never run during restoration. */
|
|
42
|
+
export function deriveChanges(
|
|
43
|
+
drawing: AnyNode,
|
|
44
|
+
derive: (event: import('./changes/types.js').ChangeDerivation) => void,
|
|
45
|
+
): () => void {
|
|
46
|
+
drawing.assertLive();
|
|
47
|
+
if (typeof derive !== 'function') throw new TypeError('Change derivation must be a function');
|
|
48
|
+
drawing.context.changes.configure(stateAdapter);
|
|
49
|
+
const callbacks = drawing.context.changes.derivations;
|
|
50
|
+
callbacks.add(derive);
|
|
51
|
+
return () => {
|
|
52
|
+
callbacks.delete(derive);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
38
55
|
export function withChanges<Result>(
|
|
39
56
|
drawing: AnyNode,
|
|
40
57
|
metadata: ChangeMetadata,
|
|
@@ -43,9 +60,81 @@ export function withChanges<Result>(
|
|
|
43
60
|
drawing.assertLive();
|
|
44
61
|
return drawing.context.changes.scope(metadata, callback, drawing);
|
|
45
62
|
}
|
|
46
|
-
export function beginChangeGroup(drawing: AnyNode, metadata: ChangeMetadata = {}) {
|
|
63
|
+
export function beginChangeGroup(drawing: AnyNode, metadata: ChangeMetadata = {}): ChangeGroup {
|
|
47
64
|
drawing.assertLive();
|
|
48
|
-
|
|
65
|
+
const owner = drawing.context.root!;
|
|
66
|
+
let id: number | undefined;
|
|
67
|
+
let changes: readonly ModelChange[] = [];
|
|
68
|
+
let invalidated = false;
|
|
69
|
+
// Subscribe before groupstart so edits made by start observers are captured too.
|
|
70
|
+
const unsubscribe = observeChanges(drawing, (event) => {
|
|
71
|
+
if (event.type === 'dispose') invalidated = true;
|
|
72
|
+
if (id === undefined && event.type === 'groupstart') id = event.group;
|
|
73
|
+
if (event.type === 'change' && event.group === id && !event.temporary)
|
|
74
|
+
changes = combineChanges([...changes, ...event.changes]);
|
|
75
|
+
});
|
|
76
|
+
let group: ReturnType<typeof drawing.context.changes.group>;
|
|
77
|
+
try {
|
|
78
|
+
group = drawing.context.changes.group(metadata);
|
|
79
|
+
id = group.id;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
unsubscribe();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
let active = true;
|
|
85
|
+
let cancelling = false;
|
|
86
|
+
let depth = 0;
|
|
87
|
+
const assertNotDisposed = () => {
|
|
88
|
+
if (invalidated)
|
|
89
|
+
throw new ChangeConflictError('Permanent disposal invalidated the change group');
|
|
90
|
+
};
|
|
91
|
+
const finish = (cancelled: boolean, restored = false) => {
|
|
92
|
+
active = false;
|
|
93
|
+
changes = [];
|
|
94
|
+
unsubscribe();
|
|
95
|
+
group.end(cancelled, restored);
|
|
96
|
+
};
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
id: group.id,
|
|
99
|
+
run<Result>(callback: () => Result): Result {
|
|
100
|
+
if (!active || cancelling) throw new Error('Change group has ended or is cancelling');
|
|
101
|
+
assertNotDisposed();
|
|
102
|
+
depth++;
|
|
103
|
+
try {
|
|
104
|
+
return group.run(callback);
|
|
105
|
+
} finally {
|
|
106
|
+
depth--;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
end(cancelled = false) {
|
|
110
|
+
if (active && !cancelling) finish(cancelled);
|
|
111
|
+
},
|
|
112
|
+
cancel() {
|
|
113
|
+
if (!active || cancelling) return;
|
|
114
|
+
if (depth) throw new Error('Finish the change callback before cancelling its group');
|
|
115
|
+
assertNotDisposed();
|
|
116
|
+
cancelling = true;
|
|
117
|
+
const failures: unknown[] = [];
|
|
118
|
+
try {
|
|
119
|
+
try {
|
|
120
|
+
if (changes.length) restoreChanges(owner, changes, 'undo');
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof ChangeObserverError) failures.push(error);
|
|
123
|
+
else throw error;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
finish(true, true);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
failures.push(error);
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
cancelling = false;
|
|
132
|
+
}
|
|
133
|
+
if (failures.length === 1) throw failures[0];
|
|
134
|
+
if (failures.length)
|
|
135
|
+
throw new AggregateError(failures, 'Change group cancellation observers failed');
|
|
136
|
+
},
|
|
137
|
+
});
|
|
49
138
|
}
|
|
50
139
|
|
|
51
140
|
/** One recorder per context; ordinary change observers are unrestricted. */
|
|
@@ -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
|
+
}
|