@lionweb/delta-protocol-client 0.7.0-beta.21
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 +6 -0
- package/README.md +138 -0
- package/dist/client.d.ts +58 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +239 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/log-types.d.ts +20 -0
- package/dist/log-types.d.ts.map +1 -0
- package/dist/log-types.js +21 -0
- package/dist/log-types.js.map +1 -0
- package/dist/low-level-client.d.ts +36 -0
- package/dist/low-level-client.d.ts.map +1 -0
- package/dist/low-level-client.js +18 -0
- package/dist/low-level-client.js.map +1 -0
- package/dist/priority-queue.d.ts +22 -0
- package/dist/priority-queue.d.ts.map +1 -0
- package/dist/priority-queue.js +81 -0
- package/dist/priority-queue.js.map +1 -0
- package/package.json +37 -0
- package/src/client.ts +339 -0
- package/src/index.ts +25 -0
- package/src/log-types.ts +48 -0
- package/src/low-level-client.ts +61 -0
- package/src/priority-queue.ts +94 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Licensed under the Apache License, Version 2.0 (the "License")
|
|
3
|
+
// you may not use this file except in compliance with the License.
|
|
4
|
+
// You may obtain a copy of the License at
|
|
5
|
+
//
|
|
6
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-FileCopyrightText: 2025 TRUMPF Laser SE and other contributors
|
|
15
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
/**
|
|
17
|
+
* @return the index at which `t` should be inserted to keep the array `ts` sorted in the numeric order determined by applying `valueFunc` to its members.
|
|
18
|
+
* In case `t` has the same numeric value as existing members of `ts`, the lowest index having that numeric value is returned.
|
|
19
|
+
* (Exported for testing purposes only!)
|
|
20
|
+
*/
|
|
21
|
+
export const insertionIndex = (ts, valueFunc, tToInsert) => {
|
|
22
|
+
const valueToInsert = valueFunc(tToInsert);
|
|
23
|
+
let low = 0;
|
|
24
|
+
let high = ts.length - 1;
|
|
25
|
+
while (low <= high) {
|
|
26
|
+
const mid = low + ((high - low) >>> 1); // not buggy! ;D
|
|
27
|
+
const midValue = valueFunc(ts[mid]);
|
|
28
|
+
if (midValue < valueToInsert) {
|
|
29
|
+
low = mid + 1;
|
|
30
|
+
}
|
|
31
|
+
else if (valueToInsert < midValue) {
|
|
32
|
+
high = mid - 1;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
return mid;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return low;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* @return a {@link PriorityQueueAcceptor} function that accepts instances of `T`,
|
|
42
|
+
* passing them to the `process` function in (min-max) priority order,
|
|
43
|
+
* as computed by the `priorityFunc`, starting with the `firstPriority`.
|
|
44
|
+
* @throws an {@link Error error} when a `T` is passed with a priority that has already been processed.
|
|
45
|
+
*/
|
|
46
|
+
export const priorityQueueAcceptor = (priorityFunc, firstPriority, process) => {
|
|
47
|
+
const ts = [];
|
|
48
|
+
let nextPriority = firstPriority;
|
|
49
|
+
return (t) => {
|
|
50
|
+
const priority = priorityFunc(t);
|
|
51
|
+
if (priority < nextPriority) {
|
|
52
|
+
throw new Error(`priority ${priority} has already been processed; (next expected priority: ${nextPriority})`);
|
|
53
|
+
}
|
|
54
|
+
const index0 = insertionIndex(ts, priorityFunc, t);
|
|
55
|
+
if (priority === nextPriority) {
|
|
56
|
+
nextPriority++;
|
|
57
|
+
process(t);
|
|
58
|
+
let index = index0;
|
|
59
|
+
let stop = false;
|
|
60
|
+
while (index < ts.length && !stop) {
|
|
61
|
+
const currentT = ts[index];
|
|
62
|
+
const currentPriority = priorityFunc(currentT);
|
|
63
|
+
if (currentPriority === nextPriority) {
|
|
64
|
+
nextPriority++;
|
|
65
|
+
process(currentT);
|
|
66
|
+
index++;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
stop = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (index > index0) {
|
|
73
|
+
ts.splice(index0, index - index0);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
ts.splice(index0, 0, t);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=priority-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"priority-queue.js","sourceRoot":"","sources":["../src/priority-queue.ts"],"names":[],"mappings":"AAAA,EAAE;AACF,iEAAiE;AACjE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,iDAAiD;AACjD,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AACjC,EAAE;AACF,sEAAsE;AACtE,sCAAsC;AAKtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAI,EAAO,EAAE,SAA2B,EAAE,SAAY,EAAU,EAAE;IAC5F,MAAM,aAAa,GAAG,SAAS,CAAC,SAAS,CAAC,CAAA;IAC1C,IAAI,GAAG,GAAG,CAAC,CAAA;IACX,IAAI,IAAI,GAAG,EAAE,CAAC,MAAM,GAAG,CAAC,CAAA;IAExB,OAAO,GAAG,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA,CAAE,gBAAgB;QACxD,MAAM,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;QAEnC,IAAI,QAAQ,GAAG,aAAa,EAAE,CAAC;YAC3B,GAAG,GAAG,GAAG,GAAG,CAAC,CAAA;QACjB,CAAC;aAAM,IAAI,aAAa,GAAG,QAAQ,EAAE,CAAC;YAClC,IAAI,GAAG,GAAG,GAAG,CAAC,CAAA;QAClB,CAAC;aAAM,CAAC;YACJ,OAAO,GAAG,CAAA;QACd,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAA;AACd,CAAC,CAAA;AAWD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAI,YAA8B,EAAE,aAAqB,EAAE,OAAqB,EAA4B,EAAE;IAC/I,MAAM,EAAE,GAAQ,EAAE,CAAA;IAClB,IAAI,YAAY,GAAG,aAAa,CAAA;IAChC,OAAO,CAAC,CAAI,EAAE,EAAE;QACZ,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;QAChC,IAAI,QAAQ,GAAG,YAAY,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,YAAY,QAAQ,yDAAyD,YAAY,GAAG,CAAC,CAAA;QACjH,CAAC;QACD,MAAM,MAAM,GAAG,cAAc,CAAC,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC,CAAA;QAClD,IAAI,QAAQ,KAAK,YAAY,EAAE,CAAC;YAC5B,YAAY,EAAE,CAAA;YACd,OAAO,CAAC,CAAC,CAAC,CAAA;YACV,IAAI,KAAK,GAAG,MAAM,CAAA;YAClB,IAAI,IAAI,GAAG,KAAK,CAAA;YAChB,OAAO,KAAK,GAAG,EAAE,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;gBAChC,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,CAAA;gBAC1B,MAAM,eAAe,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;gBAC9C,IAAI,eAAe,KAAK,YAAY,EAAE,CAAC;oBACnC,YAAY,EAAE,CAAA;oBACd,OAAO,CAAC,QAAQ,CAAC,CAAA;oBACjB,KAAK,EAAE,CAAA;gBACX,CAAC;qBAAM,CAAC;oBACJ,IAAI,GAAG,IAAI,CAAA;gBACf,CAAC;YACL,CAAC;YACD,IAAI,KAAK,GAAG,MAAM,EAAE,CAAC;gBACjB,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC,CAAA;YACrC,CAAC;QACL,CAAC;aAAM,CAAC;YACJ,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC3B,CAAC;IACL,CAAC,CAAA;AACL,CAAC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lionweb/delta-protocol-client",
|
|
3
|
+
"version": "0.7.0-beta.21",
|
|
4
|
+
"description": "Part of the implementation of a delta protocol client not tied to a specific message transport protocol",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"typings": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/LionWeb-io/lionweb-typescript.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/LionWeb-io/lionweb-typescript/issues"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"clean": "npx rimraf dist node_modules",
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"lint": "eslint src",
|
|
21
|
+
"prep:pre-release": "npm run clean && npm install && npm run build",
|
|
22
|
+
"prerelease-alpha": "npm run prep:pre-release",
|
|
23
|
+
"release-alpha": "npm publish --tag alpha",
|
|
24
|
+
"prerelease-beta": "npm run prep:pre-release",
|
|
25
|
+
"release-beta": "npm publish --tag beta",
|
|
26
|
+
"prerelease": "npm run prep:pre-release",
|
|
27
|
+
"release": "npm publish"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@lionweb/class-core": "0.7.0-beta.21",
|
|
31
|
+
"@lionweb/core": "0.7.0-beta.21",
|
|
32
|
+
"@lionweb/delta-protocol-common": "0.7.0-beta.21",
|
|
33
|
+
"@lionweb/delta-protocol-client": "0.7.0-beta.21",
|
|
34
|
+
"@lionweb/json": "0.7.0-beta.21",
|
|
35
|
+
"@lionweb/ts-utils": "0.7.0-beta.21"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
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 {
|
|
19
|
+
allNodesFrom,
|
|
20
|
+
applyDelta,
|
|
21
|
+
combinedFactoryFor,
|
|
22
|
+
DeltaReceiver,
|
|
23
|
+
IdMapping,
|
|
24
|
+
ILanguageBase,
|
|
25
|
+
INodeBase,
|
|
26
|
+
nodeBaseDeserializer,
|
|
27
|
+
NodeBaseFactory,
|
|
28
|
+
PartitionAddedDelta,
|
|
29
|
+
PartitionDeletedDelta,
|
|
30
|
+
serializeDelta
|
|
31
|
+
} from "@lionweb/class-core"
|
|
32
|
+
import { Concept } from "@lionweb/core"
|
|
33
|
+
import { LionWebId, LionWebJsonChunk } from "@lionweb/json"
|
|
34
|
+
import { byIdMap } from "@lionweb/ts-utils"
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
ansi,
|
|
38
|
+
ClientAppliedEvent,
|
|
39
|
+
ClientDidNotApplyEventFromOwnCommand,
|
|
40
|
+
ClientHadProblem,
|
|
41
|
+
ClientReceivedMessage,
|
|
42
|
+
ClientSentMessage,
|
|
43
|
+
Command,
|
|
44
|
+
deltaAsCommand,
|
|
45
|
+
DeltaOccurredOnClient,
|
|
46
|
+
Event,
|
|
47
|
+
eventToDeltaTranslator,
|
|
48
|
+
isEvent,
|
|
49
|
+
isQueryResponse,
|
|
50
|
+
QueryMessage,
|
|
51
|
+
ReconnectRequest,
|
|
52
|
+
ReconnectResponse,
|
|
53
|
+
SemanticLogger,
|
|
54
|
+
semanticLoggerFunctionFrom,
|
|
55
|
+
SignOffRequest,
|
|
56
|
+
SignOnRequest,
|
|
57
|
+
SignOnResponse,
|
|
58
|
+
SubscribeToChangingPartitionsRequest,
|
|
59
|
+
SubscribeToPartitionChangesParameters,
|
|
60
|
+
SubscribeToPartitionContentsRequest,
|
|
61
|
+
SubscribeToPartitionContentsResponse,
|
|
62
|
+
UnsubscribeFromPartitionContentsRequest
|
|
63
|
+
} from "@lionweb/delta-protocol-common"
|
|
64
|
+
import { LowLevelClient, LowLevelClientInstantiator } from "./low-level-client.js"
|
|
65
|
+
import { priorityQueueAcceptor } from "./priority-queue.js"
|
|
66
|
+
|
|
67
|
+
const { clientWarning } = ansi
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Type def. for parameters – required and optional – for instantiating a {@link LionWebClient LionWeb delta protocol client}.
|
|
72
|
+
*/
|
|
73
|
+
export type LionWebClientParameters = {
|
|
74
|
+
clientId: LionWebId
|
|
75
|
+
url: string
|
|
76
|
+
languageBases: ILanguageBase[]
|
|
77
|
+
lowLevelClientInstantiator: LowLevelClientInstantiator<Event | QueryMessage, Command | QueryMessage>
|
|
78
|
+
serializationChunk?: LionWebJsonChunk
|
|
79
|
+
instantiateDeltaReceiverForwardingTo?: (commandSender: DeltaReceiver) => DeltaReceiver
|
|
80
|
+
semanticLogger?: SemanticLogger
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Implementation of a LionWeb delta protocol client.
|
|
86
|
+
*/
|
|
87
|
+
export class LionWebClient {
|
|
88
|
+
|
|
89
|
+
private _participationId?: LionWebId // !== undefined => signed on
|
|
90
|
+
|
|
91
|
+
get participationId() {
|
|
92
|
+
return this._participationId
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private signedOff = false
|
|
96
|
+
|
|
97
|
+
private lastReceivedSequenceNumber = -1
|
|
98
|
+
// TODO could also get this from the priority queue (which would need to be adapted for that)
|
|
99
|
+
|
|
100
|
+
private constructor(
|
|
101
|
+
public readonly clientId: LionWebId,
|
|
102
|
+
public model: INodeBase[],
|
|
103
|
+
public readonly idMapping: IdMapping,
|
|
104
|
+
public readonly createNode: NodeBaseFactory,
|
|
105
|
+
private readonly effectiveReceiveDelta: DeltaReceiver,
|
|
106
|
+
private readonly lowLevelClient: LowLevelClient<Command | QueryMessage>
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
private readonly queryResolveById: { [queryId: string]: (value: QueryMessage) => void } = {}
|
|
110
|
+
|
|
111
|
+
private static readonly nodesByIdFrom = (model: INodeBase[]) =>
|
|
112
|
+
byIdMap(model.flatMap(allNodesFrom))
|
|
113
|
+
|
|
114
|
+
static async create({clientId, url, languageBases, instantiateDeltaReceiverForwardingTo, serializationChunk, semanticLogger, lowLevelClientInstantiator}: LionWebClientParameters): Promise<LionWebClient> {
|
|
115
|
+
const log = semanticLoggerFunctionFrom(semanticLogger)
|
|
116
|
+
|
|
117
|
+
let loading = true
|
|
118
|
+
let commandNumber = 0
|
|
119
|
+
const issuedCommandIds: string[] = []
|
|
120
|
+
const commandSender: DeltaReceiver = (delta) => {
|
|
121
|
+
try {
|
|
122
|
+
const serializedDelta = serializeDelta(delta)
|
|
123
|
+
log(new DeltaOccurredOnClient(clientId, serializedDelta))
|
|
124
|
+
if (!loading) {
|
|
125
|
+
const commandId = `cmd-${++commandNumber}`
|
|
126
|
+
const command = deltaAsCommand(delta, commandId)
|
|
127
|
+
if (command !== undefined) {
|
|
128
|
+
issuedCommandIds.push(commandId) // (register the ID before actually sending the command so that effectively-synchronous tests mimic the actual behavior more reliably)
|
|
129
|
+
lowLevelClient.sendMessage(command)
|
|
130
|
+
log(new ClientSentMessage(clientId, command))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (e: unknown) {
|
|
134
|
+
console.error(`error occurred during serialization of delta: ${(e as Error).message}`)
|
|
135
|
+
console.dir(delta)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const effectiveReceiveDelta = instantiateDeltaReceiverForwardingTo === undefined ? commandSender : instantiateDeltaReceiverForwardingTo(commandSender)
|
|
139
|
+
|
|
140
|
+
const deserialized = nodeBaseDeserializer(languageBases, effectiveReceiveDelta)
|
|
141
|
+
const model = serializationChunk === undefined ? [] : deserialized(serializationChunk)
|
|
142
|
+
const idMapping = new IdMapping(LionWebClient.nodesByIdFrom(model))
|
|
143
|
+
const eventAsDelta = eventToDeltaTranslator(languageBases, deserialized)
|
|
144
|
+
loading = false
|
|
145
|
+
|
|
146
|
+
const processEvent = (event: Event) => {
|
|
147
|
+
lionWebClient.lastReceivedSequenceNumber = event.sequenceNumber
|
|
148
|
+
const commandOriginatingFromSelf = event.originCommands.find(({ commandId }) => issuedCommandIds.indexOf(commandId) > -1)
|
|
149
|
+
// Note: we can't remove members from issuedCommandIds because there may be multiple events originating fom a single command.
|
|
150
|
+
if (commandOriginatingFromSelf === undefined) {
|
|
151
|
+
try {
|
|
152
|
+
const delta = eventAsDelta(event, idMapping)
|
|
153
|
+
if (delta !== undefined) {
|
|
154
|
+
try {
|
|
155
|
+
applyDelta(delta)
|
|
156
|
+
log(new ClientAppliedEvent(clientId, event))
|
|
157
|
+
} catch (e) {
|
|
158
|
+
log(new ClientHadProblem(clientId, `couldn't apply delta of type ${delta.constructor.name} because of: ${(e as Error).message}`))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (eventTranslationError) {
|
|
162
|
+
log(new ClientHadProblem(clientId, `couldn't translate event to a delta because of: ${(eventTranslationError as Error).message}\n\tdelta = ${JSON.stringify(event)}`))
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
log(new ClientDidNotApplyEventFromOwnCommand(clientId, commandOriginatingFromSelf.commandId))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const acceptEvent = priorityQueueAcceptor<Event>(({sequenceNumber}) => sequenceNumber, 0, processEvent)
|
|
170
|
+
|
|
171
|
+
const receiveMessageOnClient = (message: Event | QueryMessage) => {
|
|
172
|
+
log(new ClientReceivedMessage(clientId, message))
|
|
173
|
+
if (isQueryResponse(message)) {
|
|
174
|
+
const { queryId } = message
|
|
175
|
+
if (queryId in lionWebClient.queryResolveById) {
|
|
176
|
+
const resolveResponse = lionWebClient.queryResolveById[queryId]
|
|
177
|
+
resolveResponse(message)
|
|
178
|
+
delete lionWebClient.queryResolveById[queryId]
|
|
179
|
+
return // ~void
|
|
180
|
+
}
|
|
181
|
+
console.log(clientWarning(`client received response for a query with ID="${queryId} without having sent a corresponding request - ignoring`))
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if (isEvent(message)) {
|
|
185
|
+
acceptEvent(message)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lowLevelClient = await
|
|
191
|
+
lowLevelClientInstantiator({ url, clientId, receiveMessageOnClient /* no logging parameter */ })
|
|
192
|
+
|
|
193
|
+
const lionWebClient = new LionWebClient(
|
|
194
|
+
clientId,
|
|
195
|
+
model,
|
|
196
|
+
idMapping,
|
|
197
|
+
combinedFactoryFor(languageBases, effectiveReceiveDelta),
|
|
198
|
+
effectiveReceiveDelta,
|
|
199
|
+
lowLevelClient
|
|
200
|
+
) // Note: we need this `lionWebClient` constant non-inlined for write-access to lastReceivedSequenceNumber and queryResolveById.
|
|
201
|
+
return lionWebClient
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Sets the model held by the client to the given model, discarding the previous one.
|
|
206
|
+
* No commands will be sent because of this action.
|
|
207
|
+
*/
|
|
208
|
+
setModel(newModel: INodeBase[]) {
|
|
209
|
+
this.model = newModel
|
|
210
|
+
this.idMapping.reinitializeWith(LionWebClient.nodesByIdFrom(newModel))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async disconnect(): Promise<void> {
|
|
214
|
+
// TODO abort responses to all queries that the repository hasn't responded to?
|
|
215
|
+
await this.lowLevelClient.disconnect()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
// queries, in order of the specification (§ 6.3):
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Makes the query in the sense that the given query request is sent (as a client message),
|
|
223
|
+
* and that the `resolve` callback of the associated `Promise` is stored so the promise can be resolved,
|
|
224
|
+
* so that query call can be `await`ed.
|
|
225
|
+
*/
|
|
226
|
+
private readonly makeQuery = (queryRequest: QueryMessage): Promise<QueryMessage> =>
|
|
227
|
+
new Promise((resolveResponse, rejectResponse) => {
|
|
228
|
+
this.queryResolveById[queryRequest.queryId] = resolveResponse
|
|
229
|
+
this.lowLevelClient.sendMessage(queryRequest)
|
|
230
|
+
.catch(rejectResponse)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
async subscribeToChangingPartitions(queryId: LionWebId, parameters: SubscribeToPartitionChangesParameters): Promise<void> {
|
|
234
|
+
await this.makeQuery({
|
|
235
|
+
messageKind: "SubscribeToChangingPartitionsRequest",
|
|
236
|
+
queryId,
|
|
237
|
+
...parameters,
|
|
238
|
+
protocolMessages: []
|
|
239
|
+
} as SubscribeToChangingPartitionsRequest)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async subscribeToPartitionContents(queryId: LionWebId, partition: LionWebId): Promise<LionWebJsonChunk> { // TODO already deserialize, because we've got everything we need
|
|
243
|
+
const response = await this.makeQuery({
|
|
244
|
+
messageKind: "SubscribeToPartitionContentsRequest",
|
|
245
|
+
queryId,
|
|
246
|
+
partition,
|
|
247
|
+
protocolMessages: []
|
|
248
|
+
} as SubscribeToPartitionContentsRequest) as SubscribeToPartitionContentsResponse
|
|
249
|
+
return response.contents
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async unsubscribeFromPartitionContents(queryId: LionWebId, partition: LionWebId): Promise<void> {
|
|
253
|
+
await this.makeQuery({
|
|
254
|
+
messageKind: "UnsubscribeFromPartitionContentsRequest",
|
|
255
|
+
queryId,
|
|
256
|
+
partition,
|
|
257
|
+
protocolMessages: []
|
|
258
|
+
} as UnsubscribeFromPartitionContentsRequest)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async signOn(queryId: LionWebId, repositoryId: LionWebId): Promise<void> {
|
|
262
|
+
if (this.signedOff) {
|
|
263
|
+
return Promise.reject(new Error(`can't sign on after having signed off`))
|
|
264
|
+
}
|
|
265
|
+
const response = await this.makeQuery({
|
|
266
|
+
messageKind: "SignOnRequest",
|
|
267
|
+
queryId,
|
|
268
|
+
repositoryId,
|
|
269
|
+
deltaProtocolVersion: "2025.1",
|
|
270
|
+
clientId: this.clientId,
|
|
271
|
+
protocolMessages: []
|
|
272
|
+
} as SignOnRequest) as SignOnResponse
|
|
273
|
+
this._participationId = response.participationId
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async signOff(queryId: LionWebId): Promise<void> {
|
|
277
|
+
await this.makeQuery({
|
|
278
|
+
messageKind: "SignOffRequest",
|
|
279
|
+
queryId,
|
|
280
|
+
protocolMessages: []
|
|
281
|
+
} as SignOffRequest)
|
|
282
|
+
this.signedOff = true
|
|
283
|
+
this._participationId = undefined
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async reconnect(queryId: LionWebId, participationId: LionWebId, lastReceivedSequenceNumber: number): Promise<void> {
|
|
287
|
+
const response = await this.makeQuery({
|
|
288
|
+
messageKind: "ReconnectRequest",
|
|
289
|
+
queryId,
|
|
290
|
+
participationId,
|
|
291
|
+
lastReceivedSequenceNumber,
|
|
292
|
+
protocolMessages: []
|
|
293
|
+
} as ReconnectRequest) as ReconnectResponse
|
|
294
|
+
this._participationId = participationId
|
|
295
|
+
this.lastReceivedSequenceNumber = response.lastReceivedSequenceNumber
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
// commands, in order of the specification (§ 6.5):
|
|
300
|
+
|
|
301
|
+
private static checkWhetherPartition(node: INodeBase): void {
|
|
302
|
+
const {classifier} = node
|
|
303
|
+
if (!(classifier instanceof Concept)) {
|
|
304
|
+
throw new Error(`node with classifier ${classifier.name} from language ${classifier.language.name} is not an instance of a Concept`)
|
|
305
|
+
}
|
|
306
|
+
if (!classifier.partition) {
|
|
307
|
+
throw new Error(`classifier ${classifier.name} from language ${classifier.language.name} is not a partition`)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private checkSignedOn(): void {
|
|
312
|
+
if (this._participationId === undefined) {
|
|
313
|
+
throw new Error(`client ${this.clientId} can't send a command without being signed on`)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
addPartition(partition: INodeBase): void {
|
|
318
|
+
this.checkSignedOn()
|
|
319
|
+
LionWebClient.checkWhetherPartition(partition)
|
|
320
|
+
if (this.model.indexOf(partition) === -1) {
|
|
321
|
+
this.model.push(partition)
|
|
322
|
+
this.idMapping.updateWith(partition)
|
|
323
|
+
this.effectiveReceiveDelta(new PartitionAddedDelta(partition))
|
|
324
|
+
} // else: ignore; already done
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
deletePartition(partition: INodeBase): void {
|
|
328
|
+
this.checkSignedOn()
|
|
329
|
+
const index = this.model.indexOf(partition)
|
|
330
|
+
if (index > -1) {
|
|
331
|
+
this.model.splice(index, 1)
|
|
332
|
+
this.effectiveReceiveDelta(new PartitionDeletedDelta(partition))
|
|
333
|
+
} else {
|
|
334
|
+
throw new Error(`node with id "${partition.id}" is not a partition in the current model`)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
}
|
|
339
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
export { LionWebClient } from "./client.js"
|
|
19
|
+
export type { LionWebClientParameters } from "./client.js"
|
|
20
|
+
|
|
21
|
+
export { noOpLowLevelClientLogger } from "./log-types.js"
|
|
22
|
+
export type { MessageReceivedOnClient, MessageSentToServer, TextualLogItem, LowLevelClientLogItem, LowLevelClientLogger } from "./log-types.js"
|
|
23
|
+
|
|
24
|
+
export type { LowLevelClient, LowLevelClientInstantiator, LowLevelClientLoggingParameters, LowLevelClientParameters } from "./low-level-client.js"
|
|
25
|
+
|
package/src/log-types.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
export type MessageReceivedOnClient<TMessageForClient> = {
|
|
19
|
+
receivedOnClient: TMessageForClient
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type MessageSentToServer<TMessageForServer> = {
|
|
23
|
+
sentToServer: TMessageForServer
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type TextualLogItem = {
|
|
27
|
+
message: string
|
|
28
|
+
error?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Type def. for activity logged by a low-level client.
|
|
33
|
+
*/
|
|
34
|
+
export type LowLevelClientLogItem<TMessageForClient, TMessageForServer> =
|
|
35
|
+
| TextualLogItem
|
|
36
|
+
| MessageReceivedOnClient<TMessageForClient>
|
|
37
|
+
| MessageSentToServer<TMessageForServer>
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export type LowLevelClientLogger<TMessageForClient, TMessageForServer> =
|
|
41
|
+
(logItem: LowLevelClientLogItem<TMessageForClient, TMessageForServer>) => void
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Implementation of {@link LowLevelClientLogger} that does nothing.
|
|
46
|
+
*/
|
|
47
|
+
export const noOpLowLevelClientLogger: LowLevelClientLogger<unknown, unknown> = (_logItem) => {}
|
|
48
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
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 { LionWebId } from "@lionweb/json"
|
|
19
|
+
import { TextualLogger } from "@lionweb/delta-protocol-common"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Type def. for a client that's able to send messages.
|
|
24
|
+
*/
|
|
25
|
+
export type LowLevelClient<TMessageToServer> = {
|
|
26
|
+
sendMessage: (message: TMessageToServer) => Promise<void>
|
|
27
|
+
disconnect: () => Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type def. for parameters *required* for instantiating a {@link LowLevelClient low-level client}.
|
|
32
|
+
*/
|
|
33
|
+
export type LowLevelClientParameters<TMessageForClient> = {
|
|
34
|
+
/** The URL of the WebSocket server to connect to. */
|
|
35
|
+
url: string
|
|
36
|
+
/** An ID for the created client. */
|
|
37
|
+
clientId: LionWebId
|
|
38
|
+
/** A function that's called with a received message. */
|
|
39
|
+
receiveMessageOnClient: (message: TMessageForClient) => void
|
|
40
|
+
/** An optional {@link TextualLogger textual logger}. */
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Type def. for optional parameters for a {@link LowLevelClient low-level client} regarding logging.
|
|
45
|
+
*/
|
|
46
|
+
export type LowLevelClientLoggingParameters<TMessageForClient, TMessageToServer> = {
|
|
47
|
+
/** An optional {@link TextualLogger textual logger}. */
|
|
48
|
+
textualLogger?: TextualLogger
|
|
49
|
+
/** An optional message logger. */
|
|
50
|
+
messageLogger?: (message: (TMessageForClient | TMessageToServer)) => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Type def. for functions that instantiate a {@link LowLevelClient}.
|
|
55
|
+
*
|
|
56
|
+
* Note that the instantiator implementation is responsible for passing logging parameters.
|
|
57
|
+
*/
|
|
58
|
+
export type LowLevelClientInstantiator<TMessageForClient, TMessageToServer> =
|
|
59
|
+
(lowLevelClientParameters: LowLevelClientParameters<TMessageForClient>) =>
|
|
60
|
+
Promise<LowLevelClient<TMessageToServer>>
|
|
61
|
+
|