@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.
- package/CHANGELOG.md +6 -0
- package/README.md +24 -0
- package/dist/command-to-event.d.ts +6 -0
- package/dist/command-to-event.d.ts.map +1 -0
- package/dist/command-to-event.js +450 -0
- package/dist/command-to-event.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/repository-impl.d.ts +13 -0
- package/dist/repository-impl.d.ts.map +1 -0
- package/dist/repository-impl.js +119 -0
- package/dist/repository-impl.js.map +1 -0
- package/dist/server.d.ts +23 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +75 -0
- package/dist/server.js.map +1 -0
- package/dist/set.d.ts +5 -0
- package/dist/set.d.ts.map +1 -0
- package/dist/set.js +27 -0
- package/dist/set.js.map +1 -0
- package/package.json +38 -0
- package/src/command-to-event.ts +544 -0
- package/src/index.ts +22 -0
- package/src/repository-impl.ts +151 -0
- package/src/server.ts +104 -0
- package/src/set.ts +28 -0
|
@@ -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
|
+
|