@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/dist/LionCore_builtins.g.d.ts +2 -2
  4. package/dist/LionCore_builtins.g.d.ts.map +1 -1
  5. package/dist/LionCore_builtins.g.js +1 -1
  6. package/dist/LionCore_builtins.g.js.map +1 -1
  7. package/dist/base-types.d.ts +7 -11
  8. package/dist/base-types.d.ts.map +1 -1
  9. package/dist/base-types.js +3 -28
  10. package/dist/base-types.js.map +1 -1
  11. package/dist/convenience.d.ts +7 -7
  12. package/dist/convenience.d.ts.map +1 -1
  13. package/dist/convenience.js +7 -7
  14. package/dist/convenience.js.map +1 -1
  15. package/dist/deltas/appliers.d.ts +2 -2
  16. package/dist/deltas/appliers.d.ts.map +1 -1
  17. package/dist/deltas/appliers.js +5 -2
  18. package/dist/deltas/appliers.js.map +1 -1
  19. package/dist/deltas/compositor.d.ts +51 -0
  20. package/dist/deltas/compositor.d.ts.map +1 -0
  21. package/dist/deltas/compositor.js +95 -0
  22. package/dist/deltas/compositor.js.map +1 -0
  23. package/dist/deltas/index.d.ts +2 -1
  24. package/dist/deltas/index.d.ts.map +1 -1
  25. package/dist/deltas/index.js +2 -1
  26. package/dist/deltas/index.js.map +1 -1
  27. package/dist/deltas/inverters.d.ts.map +1 -1
  28. package/dist/deltas/inverters.js +33 -9
  29. package/dist/deltas/inverters.js.map +1 -1
  30. package/dist/deltas/receivers.d.ts +39 -0
  31. package/dist/deltas/receivers.d.ts.map +1 -0
  32. package/dist/deltas/receivers.js +60 -0
  33. package/dist/deltas/receivers.js.map +1 -0
  34. package/dist/deserializer.d.ts +5 -5
  35. package/dist/deserializer.d.ts.map +1 -1
  36. package/dist/deserializer.js +8 -19
  37. package/dist/deserializer.js.map +1 -1
  38. package/dist/factory.d.ts +10 -2
  39. package/dist/factory.d.ts.map +1 -1
  40. package/dist/factory.js +25 -16
  41. package/dist/factory.js.map +1 -1
  42. package/dist/id-mapping.d.ts +21 -3
  43. package/dist/id-mapping.d.ts.map +1 -1
  44. package/dist/id-mapping.js +37 -17
  45. package/dist/id-mapping.js.map +1 -1
  46. package/dist/textualizer.js +1 -1
  47. package/dist/textualizer.js.map +1 -1
  48. package/dist/value-managers/annotations.d.ts.map +1 -1
  49. package/dist/value-managers/annotations.js +1 -2
  50. package/dist/value-managers/annotations.js.map +1 -1
  51. package/dist/value-managers/base.d.ts +2 -2
  52. package/dist/value-managers/base.js +4 -4
  53. package/dist/value-managers/base.js.map +1 -1
  54. package/dist/value-managers/containments.d.ts +2 -2
  55. package/dist/value-managers/containments.d.ts.map +1 -1
  56. package/dist/value-managers/containments.js +89 -27
  57. package/dist/value-managers/containments.js.map +1 -1
  58. package/dist/value-managers/references.js +2 -2
  59. package/package.json +34 -34
  60. package/src/LionCore_builtins.g.ts +2 -2
  61. package/src/base-types.ts +6 -31
  62. package/src/convenience.ts +10 -10
  63. package/src/deltas/appliers.ts +5 -2
  64. package/src/deltas/compositor.ts +107 -0
  65. package/src/deltas/index.ts +3 -1
  66. package/src/deltas/inverters.ts +33 -9
  67. package/src/deltas/receivers.ts +92 -0
  68. package/src/deserializer.ts +9 -25
  69. package/src/factory.ts +31 -17
  70. package/src/id-mapping.ts +26 -5
  71. package/src/textualizer.ts +1 -1
  72. package/src/value-managers/annotations.ts +2 -2
  73. package/src/value-managers/base.ts +4 -4
  74. package/src/value-managers/containments.ts +87 -27
  75. package/src/value-managers/references.ts +2 -2
  76. package/dist/deltas/handlers.d.ts +0 -17
  77. package/dist/deltas/handlers.d.ts.map +0 -1
  78. package/dist/deltas/handlers.js +0 -43
  79. package/dist/deltas/handlers.js.map +0 -1
  80. package/src/deltas/handlers.ts +0 -64
@@ -42,7 +42,7 @@ import {
42
42
  } from "@lionweb/json";
43
43
 
44
44
  import {
45
- DeltaHandler,
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(_handleDelta?: DeltaHandler): NodeBaseFactory {
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
- DeltaHandler,
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 DeltaHandler}.
181
+ * An optionally-installed {@link DeltaReceiver}.
182
182
  */
183
- handleDelta?: DeltaHandler;
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 DeltaHandler} to pass {@link IDelta deltas} to
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 handleDelta?: DeltaHandler, parentage?: Parentage) {
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(handleDelta?: DeltaHandler): NodeBaseFactory;
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
-
@@ -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, DeltaHandler, IdMapping, ILanguageBase, INodeBase } from "./index.js"
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 handleDelta an optional {@link DeltaHandler} that gets installed on the deep-cloned nodes
60
+ * @param receiveDelta an optional {@link DeltaReceiver} that gets installed on the deep-cloned nodes
61
61
  */
62
- export const deepClonerFor = (languageBases: ILanguageBase[], handleDelta?: DeltaHandler) =>
63
- deepDuplicatorFor(languageBases, (originalNode) => originalNode.id, handleDelta);
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 handleDelta an optional {@link DeltaHandler}
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, handleDelta?: DeltaHandler, idMapping?: IdMapping): NodeDuplicator =>
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(handleDelta)(originalNode.classifier, newIdFunc(originalNode));
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 handleDelta an optional {@link DeltaHandler} that gets installed on the deep-cloned nodes
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, handleDelta?: DeltaHandler, idMapping?: IdMapping) =>
97
- deepDuplicateWith(defaultNodeDuplicatorFor(languageBases, newIdFunc, handleDelta, idMapping));
96
+ export const deepDuplicatorFor = (languageBases: ILanguageBase[], newIdFunc: NewIdFunction, receiveDelta?: DeltaReceiver, idMapping?: IdMapping) =>
97
+ deepDuplicateWith(defaultNodeDuplicatorFor(languageBases, newIdFunc, receiveDelta, idMapping));
98
98
 
@@ -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, handleDelta);
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, handleDelta);
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 &hArr; `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
+
@@ -17,7 +17,9 @@
17
17
 
18
18
  export * from "./appliers.js";
19
19
  export * from "./base.js";
20
- export * from "./handlers.js";
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
+
@@ -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
- // TODO implement
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
- // TODO implement
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
- // TODO implement
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
- // TODO implement
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
- // TODO implement
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
- // TODO implement
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
- // TODO implement
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 EntryMovedAndReplacedFromOtherReferenceInSameParentDelta(delta.parent, delta.newReference, delta.newIndex, delta.oldReference, delta.oldIndex, delta.replacedTarget, delta.movedTarget);
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
- // TODO implement
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
+
@@ -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 { DeltaHandler, IdMapping, ILanguageBase, INodeBase } from "./index.js"
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 handleDelta an optional {@link DeltaHandler} that will be injected in all {@link INodeBase nodes} created.
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[], handleDelta?: DeltaHandler): Deserializer<RootsWithIdMapping> => {
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 = languageBaseLookupFor(languageBases);
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 = factoryFor(classifier.language)(classifier, id);
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 handleDelta an optional {@link DeltaHandler} that will be injected in all {@link INodeBase nodes} created.
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[], handleDelta?: DeltaHandler): Deserializer<INodeBase[]> => {
225
- const deserializerWithIdMapping = nodeBaseDeserializerWithIdMapping(languageBases, handleDelta);
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 { DeltaHandler } from "./deltas/index.js"
21
+ import { DeltaReceiver } from "./deltas/index.js"
22
+
21
23
 
22
24
  /**
23
- * @return a {@link NodeBaseFactory factory function} that works for all given {@link ILanguageBase language bases}.
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 combinedFactoryFor = (languageBases: ILanguageBase[], handleDelta?: DeltaHandler): NodeBaseFactory => {
30
+ export const combinedLanguageBaseLookupFor = (languageBases: ILanguageBase[]): ((language: Language) => ILanguageBase) => {
26
31
  // create lookup map:
27
- const languageKey2version2factory: { [key: string]: { [version: string]: NodeBaseFactory } } = {}
32
+ const languageKey2version2base: { [key: string]: { [version: string]: ILanguageBase } } = {}
28
33
  languageBases.forEach((languageBase) => {
29
34
  const {key, version} = languageBase.language
30
- const version2factory = lazyMapGet(languageKey2version2factory, key, () => ({}))
31
- lazyMapGet(version2factory, version, () => languageBase.factory(handleDelta))
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 (classifier, id) => {
36
- const {key, version, name} = classifier.language
37
- const version2factory = languageKey2version2factory[key]
38
- if (version2factory === undefined) {
39
- throw new Error(`language ${name} with key=${key} not known`)
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 factory = version2factory[version]
42
- if (factory === undefined) {
43
- const candidateVersions = Object.keys(version2factory)
44
- throw new Error(`language ${name} with key=${key} and version=${version} not known${candidateVersions.length > 0 ? `- candidate version${candidateVersions.length > 1 ? `s` : ``}: ${candidateVersions.join(", ")}` : ``}`)
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 factory(classifier, id)
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
+