@lionweb/class-core 0.7.0-beta.12 → 0.7.0-beta.14
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 +10 -0
- package/README.md +6 -6
- package/dist/LionCore_builtins.g.d.ts +2 -2
- package/dist/LionCore_builtins.g.d.ts.map +1 -1
- package/dist/LionCore_builtins.g.js +1 -1
- package/dist/LionCore_builtins.g.js.map +1 -1
- package/dist/base-types.d.ts +7 -11
- package/dist/base-types.d.ts.map +1 -1
- package/dist/base-types.js +3 -28
- package/dist/base-types.js.map +1 -1
- package/dist/convenience.d.ts +7 -7
- package/dist/convenience.d.ts.map +1 -1
- package/dist/convenience.js +7 -7
- package/dist/convenience.js.map +1 -1
- package/dist/deltas/appliers.d.ts +2 -2
- package/dist/deltas/appliers.d.ts.map +1 -1
- package/dist/deltas/appliers.js +5 -2
- package/dist/deltas/appliers.js.map +1 -1
- package/dist/deltas/compositor.d.ts +51 -0
- package/dist/deltas/compositor.d.ts.map +1 -0
- package/dist/deltas/compositor.js +95 -0
- package/dist/deltas/compositor.js.map +1 -0
- package/dist/deltas/index.d.ts +2 -1
- package/dist/deltas/index.d.ts.map +1 -1
- package/dist/deltas/index.js +2 -1
- package/dist/deltas/index.js.map +1 -1
- package/dist/deltas/inverters.d.ts.map +1 -1
- package/dist/deltas/inverters.js +33 -9
- package/dist/deltas/inverters.js.map +1 -1
- package/dist/deltas/receivers.d.ts +39 -0
- package/dist/deltas/receivers.d.ts.map +1 -0
- package/dist/deltas/receivers.js +60 -0
- package/dist/deltas/receivers.js.map +1 -0
- package/dist/deserializer.d.ts +5 -5
- package/dist/deserializer.d.ts.map +1 -1
- package/dist/deserializer.js +8 -19
- package/dist/deserializer.js.map +1 -1
- package/dist/factory.d.ts +10 -2
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +25 -16
- package/dist/factory.js.map +1 -1
- package/dist/id-mapping.d.ts +21 -3
- package/dist/id-mapping.d.ts.map +1 -1
- package/dist/id-mapping.js +37 -17
- package/dist/id-mapping.js.map +1 -1
- package/dist/textualizer.js +1 -1
- package/dist/textualizer.js.map +1 -1
- package/dist/value-managers/annotations.d.ts.map +1 -1
- package/dist/value-managers/annotations.js +1 -2
- package/dist/value-managers/annotations.js.map +1 -1
- package/dist/value-managers/base.d.ts +2 -2
- package/dist/value-managers/base.js +4 -4
- package/dist/value-managers/base.js.map +1 -1
- package/dist/value-managers/containments.d.ts +2 -2
- package/dist/value-managers/containments.d.ts.map +1 -1
- package/dist/value-managers/containments.js +89 -27
- package/dist/value-managers/containments.js.map +1 -1
- package/dist/value-managers/references.js +2 -2
- package/package.json +34 -34
- package/src/LionCore_builtins.g.ts +2 -2
- package/src/base-types.ts +6 -31
- package/src/convenience.ts +10 -10
- package/src/deltas/appliers.ts +5 -2
- package/src/deltas/compositor.ts +107 -0
- package/src/deltas/index.ts +3 -1
- package/src/deltas/inverters.ts +33 -9
- package/src/deltas/receivers.ts +92 -0
- package/src/deserializer.ts +9 -25
- package/src/factory.ts +31 -17
- package/src/id-mapping.ts +26 -5
- package/src/textualizer.ts +1 -1
- package/src/value-managers/annotations.ts +2 -2
- package/src/value-managers/base.ts +4 -4
- package/src/value-managers/containments.ts +87 -27
- package/src/value-managers/references.ts +2 -2
- package/dist/deltas/handlers.d.ts +0 -17
- package/dist/deltas/handlers.d.ts.map +0 -1
- package/dist/deltas/handlers.js +0 -43
- package/dist/deltas/handlers.js.map +0 -1
- package/src/deltas/handlers.ts +0 -64
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from "@lionweb/json";
|
|
43
43
|
|
|
44
44
|
import {
|
|
45
|
-
|
|
45
|
+
DeltaReceiver,
|
|
46
46
|
ILanguageBase,
|
|
47
47
|
INodeBase,
|
|
48
48
|
NodeBase,
|
|
@@ -110,7 +110,7 @@ export class LionCore_builtinsBase implements ILanguageBase {
|
|
|
110
110
|
this._wiredUp = true;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
factory(
|
|
113
|
+
factory(_receiveDelta?: DeltaReceiver): NodeBaseFactory {
|
|
114
114
|
return (classifier: Classifier, _id: LionWebId) => {
|
|
115
115
|
const {language} = classifier;
|
|
116
116
|
throw new Error(`can't instantiate ${classifier.name} (key=${classifier.key}): classifier is not known in language ${language.name} (key=${language.key}, version=${language.version})`);
|
package/src/base-types.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { makeObservable } from "mobx"
|
|
|
35
35
|
import {
|
|
36
36
|
AnnotationsValueManager,
|
|
37
37
|
ContainmentValueManager,
|
|
38
|
-
|
|
38
|
+
DeltaReceiver,
|
|
39
39
|
FeatureValueManager,
|
|
40
40
|
MultiContainmentValueManager,
|
|
41
41
|
MultiReferenceValueManager,
|
|
@@ -178,9 +178,9 @@ export interface INodeBase extends Node {
|
|
|
178
178
|
get referenceTargets(): INodeBase[];
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
|
-
* An optionally-installed {@link
|
|
181
|
+
* An optionally-installed {@link DeltaReceiver}.
|
|
182
182
|
*/
|
|
183
|
-
|
|
183
|
+
receiveDelta?: DeltaReceiver;
|
|
184
184
|
|
|
185
185
|
|
|
186
186
|
/**
|
|
@@ -194,7 +194,7 @@ export interface INodeBase extends Node {
|
|
|
194
194
|
/**
|
|
195
195
|
* The base type for any node that's managed by a, potentially delta-emitting, API.
|
|
196
196
|
*
|
|
197
|
-
* It receives a {@link
|
|
197
|
+
* It receives a {@link DeltaReceiver} to pass {@link IDelta deltas} to
|
|
198
198
|
* that occur due to changes to values of its features,
|
|
199
199
|
* as well as moving among parents and deletion.
|
|
200
200
|
*
|
|
@@ -225,7 +225,7 @@ export abstract class NodeBase implements INodeBase {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
|
|
228
|
-
protected constructor(readonly classifier: Classifier, readonly id: LionWebId, readonly
|
|
228
|
+
protected constructor(readonly classifier: Classifier, readonly id: LionWebId, readonly receiveDelta?: DeltaReceiver, parentage?: Parentage) {
|
|
229
229
|
this.classifier = classifier;
|
|
230
230
|
this.id = id;
|
|
231
231
|
if (parentage) {
|
|
@@ -347,32 +347,7 @@ export type NodeBaseFactory = (classifier: Classifier, id: LionWebId) => INodeBa
|
|
|
347
347
|
*/
|
|
348
348
|
export interface ILanguageBase {
|
|
349
349
|
language: Language;
|
|
350
|
-
factory(
|
|
350
|
+
factory(receiveDelta?: DeltaReceiver): NodeBaseFactory;
|
|
351
351
|
enumLiteralFrom<T>(enumerationLiteral: EnumerationLiteral): T;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Removes the given child node from its parent, and returns its containment index.
|
|
357
|
-
*/
|
|
358
|
-
export const removeFromParent = (parent: INodeBase | undefined, child: INodeBase): number => {
|
|
359
|
-
if (parent === undefined) {
|
|
360
|
-
throw new Error(`can't remove an orphan from its parent`);
|
|
361
|
-
}
|
|
362
|
-
if (child.containment instanceof Containment) {
|
|
363
|
-
const valueManager = parent.getContainmentValueManager(child.containment);
|
|
364
|
-
if (valueManager instanceof SingleContainmentValueManager) {
|
|
365
|
-
valueManager.setDirectly(undefined);
|
|
366
|
-
return 0;
|
|
367
|
-
} else if (valueManager instanceof MultiContainmentValueManager) {
|
|
368
|
-
return valueManager.removeDirectly(child);
|
|
369
|
-
} else {
|
|
370
|
-
throw new Error(`don't know how to remove a child that's contained through a value manager of type ${valueManager.constructor.name}`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
if (child.containment === null) {
|
|
374
|
-
return parent.annotationsValueManager.removeDirectly(child);
|
|
375
|
-
}
|
|
376
|
-
throw new Error(`not going to remove a child from its parent without knowing how it's contained`);
|
|
377
|
-
}
|
|
378
|
-
|
package/src/convenience.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { incomingReferences as lwIncomingReferences, ReferenceValue } from "@lionweb/core"
|
|
19
19
|
import { LionWebId } from "@lionweb/json"
|
|
20
20
|
import { NodeDuplicator } from "./duplicator.js"
|
|
21
|
-
import { deepDuplicateWith,
|
|
21
|
+
import { deepDuplicateWith, DeltaReceiver, IdMapping, ILanguageBase, INodeBase } from "./index.js"
|
|
22
22
|
import { nodeBaseReader } from "./serializer.js"
|
|
23
23
|
|
|
24
24
|
|
|
@@ -57,10 +57,10 @@ export const incomingReferences = (targetNodeOrNodes: INodeBase | INodeBase[], r
|
|
|
57
57
|
* *without* generating new IDs.
|
|
58
58
|
* @param languageBases the {@link ILanguageBase language bases} of the languages that the nodes might come from
|
|
59
59
|
* — these are necessary for their factories
|
|
60
|
-
* @param
|
|
60
|
+
* @param receiveDelta an optional {@link DeltaReceiver} that gets installed on the deep-cloned nodes
|
|
61
61
|
*/
|
|
62
|
-
export const deepClonerFor = (languageBases: ILanguageBase[],
|
|
63
|
-
deepDuplicatorFor(languageBases, (originalNode) => originalNode.id,
|
|
62
|
+
export const deepClonerFor = (languageBases: ILanguageBase[], receiveDelta?: DeltaReceiver) =>
|
|
63
|
+
deepDuplicatorFor(languageBases, (originalNode) => originalNode.id, receiveDelta);
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
/**
|
|
@@ -73,13 +73,13 @@ export type NewIdFunction = (originalNode: INodeBase) => LionWebId;
|
|
|
73
73
|
* @return a {@link NodeDuplicator} function that does the “obvious” thing, given the parameters:
|
|
74
74
|
* @param languageBases an array of languages in the form of {@link ILanguageBase}s
|
|
75
75
|
* @param newIdFunc a function that computes a – not necessarily new – ID from a given original {@link INodeBase node}
|
|
76
|
-
* @param
|
|
76
|
+
* @param receiveDelta an optional {@link DeltaReceiver}
|
|
77
77
|
* @param idMapping an optional {@link IdMapping}
|
|
78
78
|
*/
|
|
79
|
-
export const defaultNodeDuplicatorFor = (languageBases: ILanguageBase[], newIdFunc: NewIdFunction,
|
|
79
|
+
export const defaultNodeDuplicatorFor = (languageBases: ILanguageBase[], newIdFunc: NewIdFunction, receiveDelta?: DeltaReceiver, idMapping?: IdMapping): NodeDuplicator =>
|
|
80
80
|
(originalNode: INodeBase) => {
|
|
81
81
|
const languageBase = languageBases.find((languageBase) => languageBase.language === originalNode.classifier.language)!;
|
|
82
|
-
const duplicatedNode = languageBase.factory(
|
|
82
|
+
const duplicatedNode = languageBase.factory(receiveDelta)(originalNode.classifier, newIdFunc(originalNode));
|
|
83
83
|
idMapping?.updateWith(duplicatedNode);
|
|
84
84
|
return [duplicatedNode, originalNode.setFeatures];
|
|
85
85
|
};
|
|
@@ -91,8 +91,8 @@ export const defaultNodeDuplicatorFor = (languageBases: ILanguageBase[], newIdFu
|
|
|
91
91
|
* @param languageBases the {@link ILanguageBase language bases} of the languages that the nodes might come from
|
|
92
92
|
* — these are necessary for their factories
|
|
93
93
|
* @param newIdFunc a function that computes a – not necessarily new – ID from a given original {@link INodeBase node}
|
|
94
|
-
* @param
|
|
94
|
+
* @param receiveDelta an optional {@link DeltaReceiver} that gets installed on the deep-cloned nodes
|
|
95
95
|
*/
|
|
96
|
-
export const deepDuplicatorFor = (languageBases: ILanguageBase[], newIdFunc: NewIdFunction,
|
|
97
|
-
deepDuplicateWith(defaultNodeDuplicatorFor(languageBases, newIdFunc,
|
|
96
|
+
export const deepDuplicatorFor = (languageBases: ILanguageBase[], newIdFunc: NewIdFunction, receiveDelta?: DeltaReceiver, idMapping?: IdMapping) =>
|
|
97
|
+
deepDuplicateWith(defaultNodeDuplicatorFor(languageBases, newIdFunc, receiveDelta, idMapping));
|
|
98
98
|
|
package/src/deltas/appliers.ts
CHANGED
|
@@ -469,7 +469,7 @@ export const applyDelta = deltaApplier()
|
|
|
469
469
|
* @usage should look as follows.
|
|
470
470
|
*
|
|
471
471
|
* ```typescript
|
|
472
|
-
* const {roots, idMapping} = deserializeAsLDMModelWithMapping(serializationChunk,
|
|
472
|
+
* const {roots, idMapping} = deserializeAsLDMModelWithMapping(serializationChunk, receiveDelta);
|
|
473
473
|
* applyDeltasWithLookup(idMapping, deltas);
|
|
474
474
|
* ```
|
|
475
475
|
*/
|
|
@@ -485,7 +485,7 @@ export const applyDeltasWithLookup = (idMapping: IdMapping, deltas: IDelta[], up
|
|
|
485
485
|
* @usage should look as follows.
|
|
486
486
|
*
|
|
487
487
|
* ```typescript
|
|
488
|
-
* const {roots, idMapping} = deserializeAsLDMModelWithMapping(serializationChunk,
|
|
488
|
+
* const {roots, idMapping} = deserializeAsLDMModelWithMapping(serializationChunk, receiveDelta);
|
|
489
489
|
* applyDeltaWithLookup(idMapping, delta);
|
|
490
490
|
* ```
|
|
491
491
|
*/
|
|
@@ -508,6 +508,9 @@ export const updateIdMappingWithDelta = (idMapping: IdMapping, delta: IDelta) =>
|
|
|
508
508
|
if (delta instanceof AnnotationAddedDelta) {
|
|
509
509
|
idMapping.updateWith(delta.newAnnotation);
|
|
510
510
|
}
|
|
511
|
+
if (delta instanceof PartitionAddedDelta) {
|
|
512
|
+
idMapping.updateWith(delta.newPartition);
|
|
513
|
+
}
|
|
511
514
|
// (nothing to be done: no need –yet?- to take deleted child nodes out of the ID mapping)
|
|
512
515
|
};
|
|
513
516
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Copyright 2025 TRUMPF Laser SE and other contributors
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License")
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-FileCopyrightText: 2025 TRUMPF Laser SE and other contributors
|
|
16
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
17
|
+
|
|
18
|
+
import { CompositeDelta, DeltaReceiver, IDelta } from "../index.js"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A <em>delta compositor</em> is used to <em>aggregate</em> deltas as {@link CompositeDelta composite deltas}.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const compositor = new Compositor(downstreamReceiveDelta);
|
|
27
|
+
* compositor.openComposite();
|
|
28
|
+
* // ...
|
|
29
|
+
* compositor.upstreamReceiveDelta(<delta>);
|
|
30
|
+
* // ...
|
|
31
|
+
* compositor.closeComposite();
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class DeltaCompositor {
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The {@link DeltaReceiver} implementation that either forwards to the given downstream delta handle
|
|
38
|
+
* or gathers deltas into composite deltas to forward to the downstream delta receiver later.
|
|
39
|
+
*/
|
|
40
|
+
readonly upstreamReceiveDelta: DeltaReceiver;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* An array of arrays of {@link IDelta deltas}, comprising a “stack”.
|
|
44
|
+
*
|
|
45
|
+
* Invariants:
|
|
46
|
+
* - (I1) composite has been opened ⇔ `this.deltasInComposite !== undefined` / `Array.isArray(this.deltasInComposite)`
|
|
47
|
+
* - (I2) this.deltasStack is either undefined or an array containing at least one array
|
|
48
|
+
*/
|
|
49
|
+
private deltasStack: IDelta[][] | undefined = undefined;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param downstreamReceiveDelta the {@link DeltaReceiver} that deltas get passed to.
|
|
53
|
+
* @param maximumNestingDepth the maximum depth that composites can be nested.
|
|
54
|
+
* A maximum depth of e.g. 1 means that at most 1 composite can be open at any time.
|
|
55
|
+
*/
|
|
56
|
+
constructor(private readonly downstreamReceiveDelta: DeltaReceiver, private readonly maximumNestingDepth: number = Infinity) {
|
|
57
|
+
if (maximumNestingDepth !== Infinity && (!Number.isInteger(maximumNestingDepth) || maximumNestingDepth < 0)) {
|
|
58
|
+
throw new Error(`maximum nesting depth must be a non-negative integer`);
|
|
59
|
+
}
|
|
60
|
+
this.upstreamReceiveDelta = (delta) => {
|
|
61
|
+
if (this.deltasStack === undefined) {
|
|
62
|
+
// pass on immediately:
|
|
63
|
+
this.downstreamReceiveDelta(delta);
|
|
64
|
+
} else {
|
|
65
|
+
// store for later:
|
|
66
|
+
this.deltasStack[this.deltasStack.length - 1].push(delta); // (I2)
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Opens a composite, meaning that all deltas passed to the {@link upstreamReceiveDelta} are gathered into a composite delta
|
|
73
|
+
* that gets forwarded to the downstream delta receiver as soon as the composite is closed.
|
|
74
|
+
*
|
|
75
|
+
* Composites can be nested, to the maximum depth configured through this class’ constructor,
|
|
76
|
+
* beyond which an error gets thrown.
|
|
77
|
+
*/
|
|
78
|
+
openComposite = (): void => {
|
|
79
|
+
if (this.deltasStack === undefined) {
|
|
80
|
+
this.deltasStack = []; // ==> (I1) + 1st half of (I2)
|
|
81
|
+
}
|
|
82
|
+
if (this.deltasStack.length >= this.maximumNestingDepth) {
|
|
83
|
+
throw new Error(`attempt occurred to start a nested composition exceeding the maximum nesting depth`)
|
|
84
|
+
}
|
|
85
|
+
this.deltasStack.push([]); // ==> 2nd half of (I2)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Closes the current composite, producing the deltas gathered for this composite into a composite delta.
|
|
90
|
+
* Composites can be nested, and deltas only get forwarded to the downstream delta receiver after the top-level composite has been closed.
|
|
91
|
+
*/
|
|
92
|
+
closeComposite = (): void => {
|
|
93
|
+
if (this.deltasStack === undefined) {
|
|
94
|
+
throw new Error(`attempt occurred to finish a composite without one being started`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parts = this.deltasStack.pop()!; // is an array because of (I2)
|
|
98
|
+
if (this.deltasStack.length === 0) { // last array popped from stack
|
|
99
|
+
this.deltasStack = undefined; // maintain (I2)
|
|
100
|
+
}
|
|
101
|
+
if (parts.length > 0) {
|
|
102
|
+
this.upstreamReceiveDelta(new CompositeDelta(parts));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
package/src/deltas/index.ts
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
export * from "./appliers.js";
|
|
19
19
|
export * from "./base.js";
|
|
20
|
-
export * from "./
|
|
20
|
+
export * from "./compositor.js";
|
|
21
21
|
export * from "./inverters.js";
|
|
22
|
+
export * from "./receivers.js";
|
|
22
23
|
export * from "./types.g.js";
|
|
23
24
|
export * from "./serialization/index.js";
|
|
25
|
+
|
package/src/deltas/inverters.ts
CHANGED
|
@@ -91,13 +91,22 @@ export const invertDelta = (delta: IDelta): IDelta => {
|
|
|
91
91
|
return new ChildMovedInSameContainmentDelta(delta.parent, delta.containment, delta.newIndex, delta.oldIndex, delta.movedChild);
|
|
92
92
|
}
|
|
93
93
|
if (delta instanceof ChildMovedAndReplacedFromOtherContainmentDelta) {
|
|
94
|
-
|
|
94
|
+
return new CompositeDelta([
|
|
95
|
+
new ChildMovedFromOtherContainmentDelta(delta.newParent, delta.newContainment, delta.newIndex, delta.oldParent, delta.oldContainment, delta.oldIndex, delta.movedChild),
|
|
96
|
+
new ChildAddedDelta(delta.newParent, delta.newContainment, delta.newIndex, delta.replacedChild)
|
|
97
|
+
]);
|
|
95
98
|
}
|
|
96
99
|
if (delta instanceof ChildMovedAndReplacedFromOtherContainmentInSameParentDelta) {
|
|
97
|
-
|
|
100
|
+
return new CompositeDelta([
|
|
101
|
+
new ChildMovedFromOtherContainmentInSameParentDelta(delta.parent, delta.newContainment, delta.newIndex, delta.movedChild, delta.oldContainment, delta.oldIndex),
|
|
102
|
+
new ChildAddedDelta(delta.parent, delta.newContainment, delta.newIndex, delta.replacedChild)
|
|
103
|
+
]);
|
|
98
104
|
}
|
|
99
105
|
if (delta instanceof ChildMovedAndReplacedInSameContainmentDelta) {
|
|
100
|
-
|
|
106
|
+
return new CompositeDelta([
|
|
107
|
+
new ChildMovedInSameContainmentDelta(delta.parent, delta.containment, delta.newIndex, delta.oldIndex, delta.movedChild),
|
|
108
|
+
new ChildAddedDelta(delta.parent, delta.containment, delta.newIndex, delta.replacedChild)
|
|
109
|
+
]);
|
|
101
110
|
}
|
|
102
111
|
if (delta instanceof AnnotationAddedDelta) {
|
|
103
112
|
return new AnnotationDeletedDelta(delta.parent, delta.index, delta.newAnnotation);
|
|
@@ -115,10 +124,16 @@ export const invertDelta = (delta: IDelta): IDelta => {
|
|
|
115
124
|
return new AnnotationMovedInSameParentDelta(delta.parent, delta.newIndex, delta.oldIndex, delta.movedAnnotation);
|
|
116
125
|
}
|
|
117
126
|
if (delta instanceof AnnotationMovedAndReplacedFromOtherParentDelta) {
|
|
118
|
-
|
|
127
|
+
return new CompositeDelta([
|
|
128
|
+
new AnnotationMovedFromOtherParentDelta(delta.newParent, delta.newIndex, delta.oldParent, delta.oldIndex, delta.movedAnnotation),
|
|
129
|
+
new AnnotationAddedDelta(delta.newParent, delta.newIndex, delta.replacedAnnotation)
|
|
130
|
+
]);
|
|
119
131
|
}
|
|
120
132
|
if (delta instanceof AnnotationMovedAndReplacedInSameParentDelta) {
|
|
121
|
-
|
|
133
|
+
return new CompositeDelta([
|
|
134
|
+
new AnnotationMovedInSameParentDelta(delta.parent, delta.newIndex, delta.oldIndex, delta.movedAnnotation),
|
|
135
|
+
new AnnotationAddedDelta(delta.parent, delta.newIndex, delta.replacedAnnotation)
|
|
136
|
+
]);
|
|
122
137
|
}
|
|
123
138
|
if (delta instanceof ReferenceAddedDelta) {
|
|
124
139
|
return new ReferenceDeletedDelta(delta.parent, delta.reference, delta.index, delta.newTarget);
|
|
@@ -133,19 +148,28 @@ export const invertDelta = (delta: IDelta): IDelta => {
|
|
|
133
148
|
return new EntryMovedFromOtherReferenceDelta(delta.newParent, delta.newReference, delta.newIndex, delta.oldParent, delta.oldReference, delta.oldIndex, delta.movedTarget);
|
|
134
149
|
}
|
|
135
150
|
if (delta instanceof EntryMovedFromOtherReferenceInSameParentDelta) {
|
|
136
|
-
|
|
151
|
+
return new EntryMovedFromOtherReferenceInSameParentDelta(delta.parent, delta.newReference, delta.newIndex, delta.oldReference, delta.oldIndex, delta.movedTarget);
|
|
137
152
|
}
|
|
138
153
|
if (delta instanceof EntryMovedInSameReferenceDelta) {
|
|
139
154
|
return new EntryMovedInSameReferenceDelta(delta.parent, delta.reference, delta.newIndex, delta.oldIndex, delta.movedTarget);
|
|
140
155
|
}
|
|
141
156
|
if (delta instanceof EntryMovedAndReplacedFromOtherReferenceDelta) {
|
|
142
|
-
|
|
157
|
+
return new CompositeDelta([
|
|
158
|
+
new EntryMovedFromOtherReferenceDelta(delta.newParent, delta.newReference, delta.newIndex, delta.oldParent, delta.oldReference, delta.oldIndex, delta.movedTarget),
|
|
159
|
+
new ReferenceAddedDelta(delta.newParent, delta.newReference, delta.newIndex, delta.replacedTarget)
|
|
160
|
+
]);
|
|
143
161
|
}
|
|
144
162
|
if (delta instanceof EntryMovedAndReplacedFromOtherReferenceInSameParentDelta) {
|
|
145
|
-
return new
|
|
163
|
+
return new CompositeDelta([
|
|
164
|
+
new EntryMovedFromOtherReferenceInSameParentDelta(delta.parent, delta.newReference, delta.newIndex, delta.oldReference, delta.oldIndex, delta.movedTarget),
|
|
165
|
+
new ReferenceAddedDelta(delta.parent, delta.newReference, delta.newIndex, delta.replacedTarget)
|
|
166
|
+
]);
|
|
146
167
|
}
|
|
147
168
|
if (delta instanceof EntryMovedAndReplacedInSameReferenceDelta) {
|
|
148
|
-
|
|
169
|
+
return new CompositeDelta([
|
|
170
|
+
new EntryMovedInSameReferenceDelta(delta.parent, delta.reference, delta.newIndex, delta.oldIndex, delta.movedTarget),
|
|
171
|
+
new ReferenceAddedDelta(delta.parent, delta.reference, delta.newIndex, delta.replacedTarget)
|
|
172
|
+
]);
|
|
149
173
|
}
|
|
150
174
|
if (delta instanceof CompositeDelta) {
|
|
151
175
|
return new CompositeDelta(delta.parts.map(invertDelta));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Copyright 2025 TRUMPF Laser SE and other contributors
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License")
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-FileCopyrightText: 2025 TRUMPF Laser SE and other contributors
|
|
16
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
17
|
+
|
|
18
|
+
import { asPrettyJsonString } from "@lionweb/ts-utils"
|
|
19
|
+
import { IDelta } from "./base.js"
|
|
20
|
+
import { serializeDelta } from "./serialization/index.js"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A type for functions that receive deltas.
|
|
24
|
+
*/
|
|
25
|
+
export type DeltaReceiver = (delta: IDelta) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Legacy alias for {@link DeltaReceiver}.
|
|
29
|
+
*/
|
|
30
|
+
export type DeltaHandler = (delta: IDelta) => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @return a tuple consisting of a {@link DeltaReceiver} implementation (as 1st member) that pushes any received delta unto the tuple's 2nd member: an array of {@link IDelta deltas}.
|
|
34
|
+
* @param printSerializations determines whether the deltas are serialized as JSON and printed to the JavaScript console.
|
|
35
|
+
*/
|
|
36
|
+
export const collectingDeltaReceiver = (printSerializations = false): [DeltaReceiver, IDelta[]] => {
|
|
37
|
+
const deltas: IDelta[] = [];
|
|
38
|
+
const receiveDelta: DeltaReceiver = (delta) => {
|
|
39
|
+
deltas.push(delta);
|
|
40
|
+
if (printSerializations) {
|
|
41
|
+
console.log(asPrettyJsonString(serializeDelta(delta)));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
return [receiveDelta, deltas];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Legacy alias for {@link collectingDeltaReceiver}.
|
|
49
|
+
*/
|
|
50
|
+
export const collectingDeltaHandler = collectingDeltaReceiver;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An interface for {@link DeltaReceiver delta receivers} that can be switched on and off — “latchingDeltaReceiverFrom”.
|
|
54
|
+
*/
|
|
55
|
+
export interface LatchingDeltaReceiver extends DeltaReceiver {
|
|
56
|
+
latch(emitDeltas: boolean): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Legacy alias for {@link LatchingDeltaReceiver}.
|
|
61
|
+
*/
|
|
62
|
+
export interface LatchingDeltaHandler extends LatchingDeltaReceiver {};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @return a latching version of the given {@link DeltaReceiver delta receiver}.
|
|
66
|
+
*/
|
|
67
|
+
export const latchingDeltaReceiverFrom = (receiveDelta: DeltaReceiver): LatchingDeltaReceiver => {
|
|
68
|
+
let emitDeltas = false;
|
|
69
|
+
return Object.assign(
|
|
70
|
+
(delta: IDelta) => {
|
|
71
|
+
if (emitDeltas) {
|
|
72
|
+
receiveDelta(delta);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
latch(emitDeltas_: boolean) {
|
|
77
|
+
emitDeltas = emitDeltas_;
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @return a {@link DeltaReceiver delta receiver} implementation that forwards received deltas to the given {@link DeltaReceiver delta receivers}.
|
|
86
|
+
* This is necessary e.g. for combining using the delta protocol with an undo mechanism.
|
|
87
|
+
*/
|
|
88
|
+
export const deltaReceiverForwardingTo = (...deltaReceivers: DeltaReceiver[]): DeltaReceiver =>
|
|
89
|
+
(delta) => {
|
|
90
|
+
deltaReceivers.forEach((receiveDelta) => receiveDelta(delta));
|
|
91
|
+
};
|
|
92
|
+
|
package/src/deserializer.ts
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
Containment,
|
|
22
22
|
defaultSimplisticHandler,
|
|
23
23
|
Enumeration,
|
|
24
|
-
Language,
|
|
25
24
|
MemoisingSymbolTable,
|
|
26
25
|
PrimitiveType,
|
|
27
26
|
Property,
|
|
@@ -33,7 +32,8 @@ import {
|
|
|
33
32
|
import { LionWebId, LionWebJsonChunk, LionWebJsonNode } from "@lionweb/json"
|
|
34
33
|
import { byIdMap, keepDefineds } from "@lionweb/ts-utils"
|
|
35
34
|
|
|
36
|
-
import {
|
|
35
|
+
import { DeltaReceiver, IdMapping, ILanguageBase, INodeBase } from "./index.js"
|
|
36
|
+
import { combinedLanguageBaseLookupFor } from "./factory.js"
|
|
37
37
|
import { NodesToInstall } from "./linking.js"
|
|
38
38
|
|
|
39
39
|
|
|
@@ -48,21 +48,6 @@ export type Deserializer<T> = (
|
|
|
48
48
|
) => T;
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
const languageBaseLookupFor = (languageBases: ILanguageBase[]) =>
|
|
52
|
-
(language: Language) => {
|
|
53
|
-
const languageBase = languageBases.find((languageBase) => languageBase.language === language);
|
|
54
|
-
if (languageBase === undefined) {
|
|
55
|
-
throw new Error(`language ${language.name} (with key=${language.key} and version=${language.version}) not known`);
|
|
56
|
-
}
|
|
57
|
-
return languageBase;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const factoryLookupFor = (languageBases: ILanguageBase[], handleDelta?: DeltaHandler) => {
|
|
61
|
-
const lookup = languageBaseLookupFor(languageBases);
|
|
62
|
-
return (language: Language) => lookup(language).factory(handleDelta);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
51
|
/**
|
|
67
52
|
* A quasi-tuple of the roots (of type {@link INodeBase}) of a model,
|
|
68
53
|
* and its {@link IdMapping} instance.
|
|
@@ -73,13 +58,12 @@ export type RootsWithIdMapping = { roots: INodeBase[], idMapping: IdMapping };
|
|
|
73
58
|
/**
|
|
74
59
|
* @return a {@link Deserializer} function for the given languages (given as {@link ILanguageBase}s) that returns a {@link RootsWithIdMapping}.
|
|
75
60
|
* @param languageBases the {@link ILanguageBase}s for (at least) all the languages used in the {@link LionWebJsonChunk} to deserialize, minus LionCore M3 and built-ins.
|
|
76
|
-
* @param
|
|
61
|
+
* @param receiveDelta an optional {@link DeltaReceiver} that will be injected in all {@link INodeBase nodes} created.
|
|
77
62
|
*/
|
|
78
|
-
export const nodeBaseDeserializerWithIdMapping = (languageBases: ILanguageBase[],
|
|
63
|
+
export const nodeBaseDeserializerWithIdMapping = (languageBases: ILanguageBase[], receiveDelta?: DeltaReceiver): Deserializer<RootsWithIdMapping> => {
|
|
79
64
|
|
|
80
65
|
const symbolTable = new MemoisingSymbolTable(languageBases.map(({language}) => language));
|
|
81
|
-
const languageBaseFor =
|
|
82
|
-
const factoryFor = factoryLookupFor(languageBases, handleDelta);
|
|
66
|
+
const languageBaseFor = combinedLanguageBaseLookupFor(languageBases);
|
|
83
67
|
|
|
84
68
|
return (
|
|
85
69
|
serializationChunk: LionWebJsonChunk,
|
|
@@ -98,7 +82,7 @@ export const nodeBaseDeserializerWithIdMapping = (languageBases: ILanguageBase[]
|
|
|
98
82
|
return undefined;
|
|
99
83
|
}
|
|
100
84
|
|
|
101
|
-
const node =
|
|
85
|
+
const node = languageBaseFor(classifier.language).factory(receiveDelta)(classifier, id);
|
|
102
86
|
|
|
103
87
|
properties.forEach(({property: propertyMetaPointer, value}) => {
|
|
104
88
|
const feature = symbolTable.featureMatching(classifierMetaPointer, propertyMetaPointer);
|
|
@@ -219,10 +203,10 @@ export const nodeBaseDeserializerWithIdMapping = (languageBases: ILanguageBase[]
|
|
|
219
203
|
/**
|
|
220
204
|
* @return a {@link Deserializer} function for the languages (given as {@link ILanguageBase}s) that returns the roots (of type {@link INodeBase}) of the deserialized model.
|
|
221
205
|
* @param languageBases the {@link ILanguageBase}s for (at least) all the languages used in the {@link LionWebJsonChunk} to deserialize, minus LionCore M3 and built-ins.
|
|
222
|
-
* @param
|
|
206
|
+
* @param receiveDelta an optional {@link DeltaReceiver} that will be injected in all {@link INodeBase nodes} created.
|
|
223
207
|
*/
|
|
224
|
-
export const nodeBaseDeserializer = (languageBases: ILanguageBase[],
|
|
225
|
-
const deserializerWithIdMapping = nodeBaseDeserializerWithIdMapping(languageBases,
|
|
208
|
+
export const nodeBaseDeserializer = (languageBases: ILanguageBase[], receiveDelta?: DeltaReceiver): Deserializer<INodeBase[]> => {
|
|
209
|
+
const deserializerWithIdMapping = nodeBaseDeserializerWithIdMapping(languageBases, receiveDelta);
|
|
226
210
|
return (
|
|
227
211
|
serializationChunk: LionWebJsonChunk,
|
|
228
212
|
dependentNodes: INodeBase[] = [],
|
package/src/factory.ts
CHANGED
|
@@ -15,35 +15,49 @@
|
|
|
15
15
|
// SPDX-FileCopyrightText: 2025 TRUMPF Laser SE and other contributors
|
|
16
16
|
// SPDX-License-Identifier: Apache-2.0
|
|
17
17
|
|
|
18
|
+
import { Language } from "@lionweb/core"
|
|
18
19
|
import { lazyMapGet } from "@lionweb/ts-utils"
|
|
19
20
|
import { ILanguageBase, NodeBaseFactory } from "./base-types.js"
|
|
20
|
-
import {
|
|
21
|
+
import { DeltaReceiver } from "./deltas/index.js"
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
* @return a {@link
|
|
25
|
+
* @return a function that looks up the {@link ILanguageBase language base} for the {@link Language language} passed to it,
|
|
26
|
+
* from among the given language bases.
|
|
27
|
+
* The returned function throws when the language wasn't among the languages the given bases were for.
|
|
28
|
+
* The lookup is hashmap-backed, so efficient.
|
|
24
29
|
*/
|
|
25
|
-
export const
|
|
30
|
+
export const combinedLanguageBaseLookupFor = (languageBases: ILanguageBase[]): ((language: Language) => ILanguageBase) => {
|
|
26
31
|
// create lookup map:
|
|
27
|
-
const
|
|
32
|
+
const languageKey2version2base: { [key: string]: { [version: string]: ILanguageBase } } = {}
|
|
28
33
|
languageBases.forEach((languageBase) => {
|
|
29
34
|
const {key, version} = languageBase.language
|
|
30
|
-
const
|
|
31
|
-
lazyMapGet(
|
|
32
|
-
// (Note: don't destructure factory from languageBase, as that will unbind it as "this"!)
|
|
35
|
+
const version2base = lazyMapGet(languageKey2version2base, key, () => ({}))
|
|
36
|
+
lazyMapGet(version2base, version, () => languageBase)
|
|
33
37
|
})
|
|
34
38
|
|
|
35
|
-
return (
|
|
36
|
-
const {key, version, name} =
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
throw new Error(`language ${name} with key=${key} not
|
|
39
|
+
return (language) => {
|
|
40
|
+
const {key, version, name} = language
|
|
41
|
+
const version2base = languageKey2version2base[key]
|
|
42
|
+
if (version2base === undefined) {
|
|
43
|
+
throw new Error(`language ${name} with key=${key} not registered`)
|
|
40
44
|
}
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
const candidateVersions = Object.keys(
|
|
44
|
-
throw new Error(`language ${name} with key=${key} and version=${version} not
|
|
45
|
+
const base = version2base[version]
|
|
46
|
+
if (base === undefined) {
|
|
47
|
+
const candidateVersions = Object.keys(version2base)
|
|
48
|
+
throw new Error(`language ${name} with key=${key} and version=${version} not registered${candidateVersions.length > 0 ? `- candidate version${candidateVersions.length > 1 ? `s` : ``}: ${candidateVersions.join(", ")}` : ``}`)
|
|
45
49
|
}
|
|
46
|
-
return
|
|
50
|
+
return base
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @return a {@link NodeBaseFactory factory function} that works for all given {@link ILanguageBase language bases}.
|
|
57
|
+
*/
|
|
58
|
+
export const combinedFactoryFor = (languageBases: ILanguageBase[], receiveDelta?: DeltaReceiver): NodeBaseFactory => {
|
|
59
|
+
const baseOf = combinedLanguageBaseLookupFor(languageBases)
|
|
60
|
+
return (classifier, id) =>
|
|
61
|
+
baseOf(classifier.language).factory(receiveDelta)(classifier, id)
|
|
62
|
+
}
|
|
63
|
+
|