@lionweb/delta-protocol-repository-ws 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.
@@ -0,0 +1,151 @@
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
+ Command,
20
+ Event,
21
+ QueryMessage,
22
+ RepositoryReceivedMessage,
23
+ SemanticLogger,
24
+ semanticLoggerFunctionFrom,
25
+ SignOffRequest,
26
+ SignOffResponse,
27
+ SignOnRequest,
28
+ SignOnResponse
29
+ } from "@lionweb/delta-protocol-common"
30
+ import { LionWebId } from "@lionweb/json"
31
+
32
+ import { createWebSocketServer, LowLevelServer } from "./server.js"
33
+ import { commandAsEvent } from "./command-to-event.js"
34
+
35
+
36
+ export type LionWebRepositoryParameters = {
37
+ port: number
38
+ semanticLogger?: SemanticLogger
39
+ }
40
+
41
+ type ClientMetadata = {
42
+ participationId: LionWebId
43
+ clientId: LionWebId
44
+ }
45
+
46
+ export class LionWebRepository {
47
+
48
+ constructor(private readonly lowLevelServer: LowLevelServer<Event>) {}
49
+
50
+ static async create({port, semanticLogger}: LionWebRepositoryParameters) {
51
+ const log = semanticLoggerFunctionFrom(semanticLogger)
52
+
53
+ let nextParticipationIdSequenceNumber = 0
54
+ const receiveMessageOnRepository = (clientMetadata: Partial<ClientMetadata>, message: Command | QueryMessage) => {
55
+ log(new RepositoryReceivedMessage({ ...clientMetadata}, message))
56
+ const checkedClientMetadata = (): ClientMetadata => {
57
+ if (clientMetadata.participationId === undefined) {
58
+ throw new Error(`can't process an event if no participation has started`) // TODO instead: log an item, and fall through
59
+ }
60
+ // (now .clientId must be !== undefined too:)
61
+ return clientMetadata as ClientMetadata
62
+ }
63
+ switch (message.messageKind) {
64
+ case "SignOnRequest": {
65
+ const { clientId, queryId } = message as SignOnRequest
66
+ clientMetadata.participationId = `participation-${String.fromCharCode(97 + (nextParticipationIdSequenceNumber++))}`
67
+ clientMetadata.clientId = clientId
68
+ return {
69
+ messageKind: "SignOnResponse",
70
+ queryId,
71
+ participationId: clientMetadata.participationId,
72
+ protocolMessages: []
73
+ } as SignOnResponse
74
+ }
75
+ case "SignOffRequest": {
76
+ const { queryId } = message as SignOffRequest
77
+ clientMetadata.participationId = undefined
78
+ return {
79
+ messageKind: "SignOffResponse",
80
+ queryId,
81
+ protocolMessages: []
82
+ } as SignOffResponse
83
+ }
84
+ // all commands, in order of the specification (§ 6.5):
85
+ /*
86
+ * **DEV note**: run
87
+ *
88
+ * $ node src/code-reading/command-message-kinds.js
89
+ *
90
+ * inside the build package to generate the following cases.
91
+ */
92
+ case "AddPartition":
93
+ case "DeletePartition":
94
+ case "ChangeClassifier":
95
+ case "AddProperty":
96
+ case "DeleteProperty":
97
+ case "ChangeProperty":
98
+ case "AddChild":
99
+ case "DeleteChild":
100
+ case "ReplaceChild":
101
+ case "MoveChildFromOtherContainment":
102
+ case "MoveChildFromOtherContainmentInSameParent":
103
+ case "MoveChildInSameContainment":
104
+ case "MoveAndReplaceChildFromOtherContainment":
105
+ case "MoveAndReplaceChildFromOtherContainmentInSameParent":
106
+ case "MoveAndReplaceChildInSameContainment":
107
+ case "AddAnnotation":
108
+ case "DeleteAnnotation":
109
+ case "ReplaceAnnotation":
110
+ case "MoveAnnotationFromOtherParent":
111
+ case "MoveAnnotationInSameParent":
112
+ case "MoveAndReplaceAnnotationFromOtherParent":
113
+ case "MoveAndReplaceAnnotationInSameParent":
114
+ case "AddReference":
115
+ case "DeleteReference":
116
+ case "ChangeReference":
117
+ case "MoveEntryFromOtherReference":
118
+ case "MoveEntryFromOtherReferenceInSameParent":
119
+ case "MoveEntryInSameReference":
120
+ case "MoveAndReplaceEntryFromOtherReference":
121
+ case "MoveAndReplaceEntryFromOtherReferenceInSameParent":
122
+ case "MoveAndReplaceEntryInSameReference":
123
+ case "AddReferenceResolveInfo":
124
+ case "DeleteReferenceResolveInfo":
125
+ case "ChangeReferenceResolveInfo":
126
+ case "AddReferenceTarget":
127
+ case "DeleteReferenceTarget":
128
+ case "ChangeReferenceTarget":
129
+ case "CompositeCommand":
130
+ {
131
+ lowLevelServer.broadcastMessage(commandAsEvent(message as Command, checkedClientMetadata().participationId)) // FIXME not correct: message to broadcast to a particular client holds sequence number for that particular participation
132
+ return undefined
133
+ }
134
+ default:
135
+ throw new Error(`can't handle message of kind "${message.messageKind}"`) // TODO instead: log an item, and fall through
136
+ }
137
+ }
138
+ const lowLevelServer = createWebSocketServer<Partial<ClientMetadata>, (Command | QueryMessage), (void | QueryMessage), Event>(
139
+ port,
140
+ (_) => ({}), // (leave values undefined – they're set later)
141
+ receiveMessageOnRepository
142
+ )
143
+ return new LionWebRepository(lowLevelServer)
144
+ }
145
+
146
+ async shutdown() {
147
+ await this.lowLevelServer.shutdown()
148
+ }
149
+
150
+ }
151
+
package/src/server.ts ADDED
@@ -0,0 +1,104 @@
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
+ TextualLogger,
20
+ textualLoggerFunctionFrom,
21
+ tryParseJson,
22
+ wrappedAsPromise
23
+ } from "@lionweb/delta-protocol-common"
24
+ import { asMinimalJsonString } from "@lionweb/ts-utils"
25
+ import { IncomingMessage } from "http"
26
+ import { WebSocketServer } from "ws"
27
+
28
+
29
+ import { setMap } from "./set.js"
30
+
31
+
32
+ /**
33
+ * Type definition for a low“-level” server that's able to broadcast messages.
34
+ * “Low-level” essentially means “wire protocol”, here.
35
+ */
36
+ export type LowLevelServer<TBroadcastMessage> = {
37
+ broadcastMessage: (message: TBroadcastMessage) => Promise<void> // TODO in general, we want to modify the message _per client_!
38
+ shutdown: () => Promise<void>
39
+ }
40
+
41
+ /**
42
+ * @return a WebSocket-driven {@link server}.
43
+ * @param port The port for clients to connect.
44
+ * @param clientMetadataFrom A function that computes metadata for a (just-)connected client from the associated {@link IncomingMessage request object}.
45
+ * @param receiveMessageOnServer A function that's called with the client's metadata and a received message.
46
+ * @param optionalTextualLogger An optional {@link TextualLogger textual logger}.
47
+ */
48
+ export const createWebSocketServer = <TClientMetadata, TMessageForServer, TResponse, TBroadcastMessage>(
49
+ port: number,
50
+ clientMetadataFrom: (request: IncomingMessage) => TClientMetadata,
51
+ receiveMessageOnServer: (clientMetadata: TClientMetadata, message: TMessageForServer) => TResponse,
52
+ optionalTextualLogger?: TextualLogger
53
+ ): LowLevelServer<TBroadcastMessage> => {
54
+ const webSocketServer = new WebSocketServer({ port })
55
+ const log = textualLoggerFunctionFrom(optionalTextualLogger)
56
+ log("WebSocketServer started")
57
+ webSocketServer.on("connection", (webSocket, request) => {
58
+ log(`a client connected`)
59
+ const clientMetadata = clientMetadataFrom(request)
60
+ webSocket.on("message", (messageText: string) => {
61
+ log(`received a message from client: ${messageText}`)
62
+ const response = receiveMessageOnServer(clientMetadata, tryParseJson(messageText, log) as TMessageForServer)
63
+ if (response !== undefined) {
64
+ const responseText = asMinimalJsonString(response)
65
+ // send back to this client (only):
66
+ webSocket.send(responseText) // TODO handle error
67
+ }
68
+ })
69
+ })
70
+ return {
71
+ // TODO rethink broadcasting: need to make a different message _per client_, based on its metadata (which then should include nextSequenceNumber)
72
+ broadcastMessage: async (message: TBroadcastMessage) => {
73
+ const messageText = asMinimalJsonString(message)
74
+ log(`broadcasting message to all clients: ${messageText}`)
75
+ await Promise.all(setMap(webSocketServer.clients, (client) =>
76
+ wrappedAsPromise((callback) => {
77
+ client.send(messageText, callback)
78
+ })
79
+ ))
80
+ },
81
+ shutdown: () => {
82
+ log(`shutting down server`)
83
+ return wrappedAsPromise((callback) => {
84
+ webSocketServer.clients.forEach((webSocket) => {
85
+ webSocket.close()
86
+ process.nextTick(() => {
87
+ const state = webSocket.readyState
88
+ if (state === webSocket.OPEN || state === webSocket.CLOSING) {
89
+ webSocket.terminate()
90
+ }
91
+ })
92
+ })
93
+ webSocketServer.close(callback)
94
+ })
95
+ }
96
+ }
97
+ }
98
+
99
+
100
+ /**
101
+ * @return the URL for a WebSocket server hosted on `localhost` on the given `port`.
102
+ */
103
+ export const wsLocalhostUrl = (port: number) => `ws://localhost:${port}`
104
+
package/src/set.ts ADDED
@@ -0,0 +1,28 @@
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
+ /**
19
+ * @return an array of the mapped values of the given set.
20
+ */
21
+ export const setMap = <T, R>(set: Set<T>, mapFunc: (t: T) => R): R[] => {
22
+ const rs: R[] = []
23
+ set.forEach((t) => {
24
+ rs.push(mapFunc(t))
25
+ })
26
+ return rs
27
+ }
28
+