@lionweb/delta-protocol-test-cli 0.7.0-beta.13 → 0.7.0-beta.15

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/src/cli-client.ts CHANGED
@@ -19,36 +19,51 @@
19
19
 
20
20
  import { argv, exit } from "process"
21
21
  import { Concept } from "@lionweb/core"
22
- import { LionWebClient, wsLocalhostUrl } from "@lionweb/delta-protocol-impl"
22
+ import {
23
+ Command,
24
+ createWebSocketClient,
25
+ Event,
26
+ LionWebClient,
27
+ QueryMessage,
28
+ wsLocalhostUrl
29
+ } from "@lionweb/delta-protocol-impl"
30
+ import { semanticConsoleLogger, semanticLogItemStorer } from "@lionweb/delta-protocol-impl/dist/semantic-logging.js"
31
+ import { clientInfo, genericError } from "@lionweb/delta-protocol-impl/dist/utils/ansi.js"
32
+ import { combine } from "@lionweb/delta-protocol-impl/dist/utils/procedure.js"
33
+ import { LowLevelClientLogItem } from "@lionweb/delta-protocol-impl/dist/web-socket/client-log-types.js"
34
+ import { writeJsonAsFile } from "@lionweb/utilities"
23
35
  import { runAsApp, tryParseInteger } from "./common.js"
24
36
  import { recognizedTasks, taskExecutor } from "./tasks.js"
25
- import { clientInfo, withStylesApplied } from "@lionweb/delta-protocol-impl/dist/utils/ansi.js"
26
- import { combine } from "@lionweb/delta-protocol-impl/dist/utils/procedure.js"
27
- import { ShapesBase } from "./gen/Shapes.g.js"
28
37
  import { TestLanguageBase } from "./gen/TestLanguage.g.js"
29
- import { semanticConsoleLogger, semanticLogItemStorer } from "@lionweb/delta-protocol-impl/dist/semantic-logging.js"
30
38
 
31
- const shapesLanguageBase = ShapesBase.INSTANCE
32
39
  const testLanguageBase = TestLanguageBase.INSTANCE
33
- const languageBases = [shapesLanguageBase, testLanguageBase]
40
+ const languageBases = [testLanguageBase]
34
41
 
35
42
  const boldRedIf = (apply: boolean, text: string) =>
36
- apply ? withStylesApplied("bold", "red")(text) : text
43
+ apply ? genericError(text) : text
37
44
 
38
45
  const partitionConcepts: Record<string, Concept> = Object.fromEntries(
39
- [testLanguageBase.DataTypeTestConcept, shapesLanguageBase.Geometry, testLanguageBase.LinkTestConcept]
46
+ [testLanguageBase.DataTypeTestConcept, testLanguageBase.LinkTestConcept]
40
47
  .map((concept) => [concept.name, concept])
41
48
  )
42
49
 
43
- if (argv.length < 5) { // $ node dist/cli-client.js <port> <clientID> <partitionConcept> [tasks]
50
+ // $ node dist/cli-client.js <port> <clientID> <partitionConcept> [tasks] [--${protocolLogOptionPrefix}=<path>]
51
+
52
+ const protocolLogOptionPrefix = "--protocol-log="
53
+ const protocolLogPathIndex = argv.findIndex((argument) => argument.startsWith(protocolLogOptionPrefix))
54
+ // arguments without "node" (index=0), "dist/cli-client.js" (index=1), and `${protocolLogOptionPrefix}=<path>` (optional)
55
+ const trueArguments = argv.filter((_, index) => !(index === 0 || index === 1 || index === protocolLogPathIndex))
56
+
57
+ if (trueArguments.length < 3) {
44
58
  console.log(
45
59
  `A Node.js-based app that implements a LionWeb delta protocol client.
46
60
 
47
- Parameters (${boldRedIf(true, "bold red")} are missing):
48
- - ${boldRedIf(argv.length < 3, `<port>: the port of the WebSocket where the LionWeb delta protocol repository is running on localhost`)}
49
- - ${boldRedIf(argv.length < 4, `<clientID>: the ID that the client identifies itself with at the repository`)}
50
- - ${boldRedIf(argv.length < 5, `<partitionConcept>: the name of a partition concept that gets instantiated as the model's primary partition — one of: ${Object.keys(partitionConcepts).join(", ")}`)}
51
- - ${boldRedIf(argv.length < 6, `[tasks]: a comma-separated list of tasks — one of ${Object.keys(recognizedTasks).sort().join(", ")}`)}
61
+ Parameters (${genericError("bold red")} are missing):
62
+ - ${boldRedIf(trueArguments.length < 1, `<port>: the port of the WebSocket where the LionWeb delta protocol repository is running on localhost`)}
63
+ - ${boldRedIf(trueArguments.length < 2, `<clientID>: the ID that the client identifies itself with at the repository`)}
64
+ - ${boldRedIf(trueArguments.length < 3, `<partitionConcept>: the name of a partition concept that gets instantiated as the model's primary partition — one of: ${Object.keys(partitionConcepts).join(", ")}`)}
65
+ - ${boldRedIf(trueArguments.length < 4, `[tasks]: a comma-separated list of tasks — one of ${Object.keys(recognizedTasks).sort().join(", ")}`)}
66
+ - ${boldRedIf(protocolLogPathIndex === -1, `${protocolLogOptionPrefix}=<path>: option to configure that the client logs all messages exchanged with the repository to a file with the given path`)}
52
67
 
53
68
  ASSUMPTION: the initial (states of the models) on client(s) and repository are identical!
54
69
  `)
@@ -59,22 +74,40 @@ const port = tryParseInteger(argv[2])
59
74
  const clientId = argv[3]
60
75
  const partitionConcept = argv[4]
61
76
  if (!(partitionConcept in partitionConcepts)) {
62
- console.log(boldRedIf(true, `unknown partition concept specified: ${partitionConcept} — must be one of: ${Object.keys(partitionConcepts).join(", ")}`))
77
+ console.error(genericError(`unknown partition concept specified: ${partitionConcept} — must be one of: ${Object.keys(partitionConcepts).join(", ")}`))
63
78
  exit(2)
64
79
  }
65
- const tasks = argv[5]?.split(",") ?? [] // unknown tasks will be ignored
80
+ const tasks = argv[5]?.split(",") ?? []
66
81
  console.log(clientInfo(`tasks provided: ${tasks.length === 0 ? "none" : tasks.join(", ")}`))
82
+ const infoUnrecognizedTasks = tasks
83
+ .map((task, index) => task in recognizedTasks ? undefined : [task, index] as [string, number])
84
+ .filter((taskInfoOrUndefined) => taskInfoOrUndefined !== undefined)
85
+ if (infoUnrecognizedTasks.length > 0) {
86
+ console.error(genericError(`unrecognized tasks encountered:`))
87
+ infoUnrecognizedTasks.forEach(([task, index]) => {
88
+ console.error(genericError(`\t${task} (#${index + 1})`))
89
+ })
90
+ exit(2)
91
+ }
67
92
 
68
93
  await runAsApp(async () => {
69
94
  const url = wsLocalhostUrl(port)
70
95
 
71
96
  const [storingLogger, semanticLogItems] = semanticLogItemStorer()
97
+ const logItems: LowLevelClientLogItem<unknown, unknown>[] = []
72
98
 
73
99
  const lionWebClient = await LionWebClient.create({
74
100
  clientId,
75
101
  url,
76
102
  languageBases,
77
- semanticLogger: combine(semanticConsoleLogger, storingLogger)
103
+ semanticLogger: combine(semanticConsoleLogger, storingLogger),
104
+ lowLevelClientInstantiator: async (lowLevelClientParameters) =>
105
+ await createWebSocketClient<(Event | QueryMessage), (Command | QueryMessage)>(
106
+ lowLevelClientParameters,
107
+ (logItem) => {
108
+ logItems.push(logItem)
109
+ }
110
+ )
78
111
  })
79
112
 
80
113
  console.log(clientInfo(`LionWeb delta protocol client (with ID=${clientId}) connecting to repository on ${url} - press Ctrl+C to terminate`))
@@ -91,6 +124,10 @@ await runAsApp(async () => {
91
124
  await executeTask(task, queryId())
92
125
  }
93
126
 
127
+ if (protocolLogPathIndex > -1) {
128
+ writeJsonAsFile(argv[protocolLogPathIndex].substring(protocolLogOptionPrefix.length), logItems)
129
+ }
130
+
94
131
  return () => {
95
132
  return lionWebClient.disconnect()
96
133
  }
package/src/tasks.ts CHANGED
@@ -18,35 +18,28 @@
18
18
  import { INodeBase } from "@lionweb/class-core"
19
19
  import { LionWebClient } from "@lionweb/delta-protocol-impl"
20
20
  import { LionWebId } from "@lionweb/json"
21
- import { ClientAppliedEvent, ISemanticLogItem } from "@lionweb/delta-protocol-impl/dist/semantic-logging.js"
22
- import { withStylesApplied } from "@lionweb/delta-protocol-impl/dist/utils/ansi.js"
21
+ import { lastOfArray } from "@lionweb/ts-utils"
22
+ import { ClientReceivedMessage, ISemanticLogItem } from "@lionweb/delta-protocol-impl/dist/semantic-logging.js"
23
+ import { clientInfo, genericWarning } from "@lionweb/delta-protocol-impl/dist/utils/ansi.js"
23
24
  import { waitUntil } from "@lionweb/delta-protocol-impl/dist/utils/async.js"
24
- import { Documentation, Geometry, ShapesBase } from "./gen/Shapes.g.js"
25
- import {
26
- DataTypeTestConcept,
27
- LinkTestConcept,
28
- TestAnnotation,
29
- TestLanguageBase
30
- } from "./gen/TestLanguage.g.js"
31
-
32
-
33
- const lastOf = <T>(ts: T[]): T => {
34
- if (ts.length === 0) {
35
- throw new Error(`empty array doesn't have a last element`)
36
- }
37
- return ts[ts.length - 1]
38
- }
25
+ import { DataTypeTestConcept, LinkTestConcept, TestAnnotation, TestLanguageBase } from "./gen/TestLanguage.g.js"
39
26
 
40
27
 
28
+ /**
29
+ * **DEV note**: run
30
+ *
31
+ * $ node src/code-reading/tasks-from-csharp.js
32
+ *
33
+ * inside the build package to generate the contents of the following object.
34
+ */
41
35
  export const recognizedTasks: Record<string, boolean> = {
42
36
  "SignOn": true,
43
37
  "SignOff": true,
44
38
  "Wait": true,
45
- "AddDocs": true,
46
- "SetDocsText": true,
47
39
  "AddStringValue_0_1": true,
48
40
  "SetStringValue_0_1": true,
49
41
  "DeleteStringValue_0_1": true,
42
+ "AddName_Containment_0_1": true,
50
43
  "AddAnnotation": true,
51
44
  "AddAnnotations": true,
52
45
  "AddAnnotation_to_Containment_0_1": true,
@@ -63,6 +56,7 @@ export const recognizedTasks: Record<string, boolean> = {
63
56
  "AddContainment_0_1_Containment_0_1": true,
64
57
  "AddContainment_1_Containment_0_1": true,
65
58
  "AddContainment_0_n": true,
59
+ "AddContainment_0_n_Containment_0_n": true,
66
60
  "AddContainment_1_n": true,
67
61
  "MoveAndReplaceChildFromOtherContainment_Single": true,
68
62
  "MoveAndReplaceChildFromOtherContainmentInSameParent_Single": true,
@@ -71,24 +65,24 @@ export const recognizedTasks: Record<string, boolean> = {
71
65
  "MoveChildFromOtherContainment_Single": true,
72
66
  "MoveChildFromOtherContainment_Multiple": true,
73
67
  "MoveChildFromOtherContainmentInSameParent_Single": true,
74
- "MoveChildFromOtherContainmentInSameParent_Multiple": true,
75
- "AddPartition": true
68
+ "AddPartition": true,
69
+ "MoveChildFromOtherContainmentInSameParent_Multiple": true
76
70
  }
77
71
 
78
72
 
79
- const shapesLanguageBase = ShapesBase.INSTANCE
80
73
  const testLanguageBase = TestLanguageBase.INSTANCE
81
74
 
82
75
 
83
76
  export const taskExecutor = (lionWebClient: LionWebClient, partition: INodeBase, semanticLogItems: ISemanticLogItem[]) => {
84
- const numberOfAppliedEvents = () =>
85
- semanticLogItems.filter((item) => item instanceof ClientAppliedEvent).length
86
77
 
87
- const waitForReceived = async (delta: number) => {
88
- const expectedNumber = numberOfAppliedEvents() + delta // (precompute here)
89
- return waitUntil(10, () => numberOfAppliedEvents() >= expectedNumber)
78
+ const numberOfReceivedMessages = () =>
79
+ semanticLogItems.filter((item) => item instanceof ClientReceivedMessage).length
80
+
81
+ const waitForReceivedMessages = async (numberOfMessagesToReceive: number) => {
82
+ const expectedNumber = numberOfReceivedMessages() + numberOfMessagesToReceive // (precompute here)
83
+ return waitUntil(10, () => numberOfReceivedMessages() >= expectedNumber)
90
84
  .then(() => {
91
- console.log(withStylesApplied("italic")(`(client applied (the deltas from) a total of ${numberOfAppliedEvents()} events so far)`))
85
+ console.log(clientInfo(`(client "${lionWebClient.clientId}" received a total of ${numberOfReceivedMessages()} messages so far)`))
92
86
  })
93
87
  }
94
88
 
@@ -100,116 +94,126 @@ export const taskExecutor = (lionWebClient: LionWebClient, partition: INodeBase,
100
94
  ? partition as LinkTestConcept
101
95
  : lionWebClient.createNode(testLanguageBase.LinkTestConcept, id) as LinkTestConcept
102
96
 
103
- return async (task: string, queryId: string) => {
97
+ return async (task: keyof typeof recognizedTasks, queryId: string) => {
98
+ console.log(clientInfo(`client "${lionWebClient.clientId}" is executing task "${task}"`))
104
99
  switch (task) {
105
100
  case "SignOn":
106
- return await lionWebClient.signOn(queryId)
101
+ return await lionWebClient.signOn(queryId, "myRepo")
107
102
  case "SignOff":
108
103
  return await lionWebClient.signOff(queryId)
109
104
  case "Wait": {
110
- return waitForReceived(1)
111
- }
112
- case "AddDocs": {
113
- (partition as Geometry).documentation = lionWebClient.createNode(shapesLanguageBase.Documentation, "documentation") as Documentation
114
- return waitForReceived(1)
115
- }
116
- case "SetDocsText": {
117
- (partition as Geometry).documentation!.text = "hello there"
118
- return waitForReceived(1)
105
+ return waitForReceivedMessages(1)
119
106
  }
120
107
  case "AddStringValue_0_1":
121
108
  (partition as DataTypeTestConcept).stringValue_0_1 = "new property"
122
- return waitForReceived(1)
109
+ return waitForReceivedMessages(1)
123
110
  case "SetStringValue_0_1":
124
111
  (partition as DataTypeTestConcept).stringValue_0_1 = "changed property"
125
- return waitForReceived(1)
112
+ return waitForReceivedMessages(1)
126
113
  case "DeleteStringValue_0_1":
127
114
  (partition as DataTypeTestConcept).stringValue_0_1 = undefined
128
- return waitForReceived(1)
115
+ return waitForReceivedMessages(1)
116
+ case "AddName_Containment_0_1":
117
+ linkTestConcept().containment_0_1!.name = "my name"
118
+ return waitForReceivedMessages(1)
129
119
  case "AddAnnotation":
130
120
  linkTestConcept().addAnnotation(annotation("annotation"))
131
- return waitForReceived(1)
121
+ return waitForReceivedMessages(1)
132
122
  case "AddAnnotations":
133
123
  linkTestConcept().addAnnotation(annotation("annotation0")); // (keep ;!)
134
124
  linkTestConcept().addAnnotation(annotation("annotation1"))
135
- return waitForReceived(2)
125
+ return waitForReceivedMessages(2)
136
126
  case "AddAnnotation_to_Containment_0_1":
137
127
  linkTestConcept().containment_0_1!.addAnnotation(annotation("annotation"))
138
- return waitForReceived(1)
128
+ return waitForReceivedMessages(1)
139
129
  case "DeleteAnnotation":
140
130
  linkTestConcept().removeAnnotation(linkTestConcept().annotations[0])
141
- return waitForReceived(1)
131
+ return waitForReceivedMessages(1)
142
132
  case "MoveAnnotationInSameParent":
143
- linkTestConcept().insertAnnotationAtIndex(lastOf(linkTestConcept().annotations), 0)
144
- return waitForReceived(1)
133
+ linkTestConcept().insertAnnotationAtIndex(lastOfArray(linkTestConcept().annotations), 0)
134
+ return waitForReceivedMessages(1)
145
135
  case "MoveAnnotationFromOtherParent":
146
136
  linkTestConcept().addAnnotation(linkTestConcept().containment_0_1!.annotations[0])
147
- return waitForReceived(1)
137
+ return waitForReceivedMessages(1)
148
138
  case "AddReference_0_1_to_Containment_0_1":
149
139
  linkTestConcept().reference_0_1 = linkTestConcept().containment_0_1
150
- return waitForReceived(1)
140
+ return waitForReceivedMessages(1)
151
141
  case "AddReference_0_1_to_Containment_1":
152
142
  linkTestConcept().reference_0_1 = linkTestConcept().containment_1
153
- return waitForReceived(1)
143
+ return waitForReceivedMessages(1)
154
144
  case "DeleteReference_0_1":
155
145
  linkTestConcept().reference_0_1 = undefined
156
- return waitForReceived(1)
146
+ return waitForReceivedMessages(1)
157
147
  case "AddContainment_0_1":
158
148
  linkTestConcept().containment_0_1 = linkTestConcept("containment_0_1")
159
- return waitForReceived(1)
149
+ return waitForReceivedMessages(1)
160
150
  case "AddContainment_1":
161
151
  linkTestConcept().containment_1 = linkTestConcept("containment_1")
162
- return waitForReceived(1)
152
+ return waitForReceivedMessages(1)
163
153
  case "ReplaceContainment_0_1":
164
- linkTestConcept().containment_0_1 = linkTestConcept("substitute")
165
- return waitForReceived(1)
154
+ linkTestConcept().replaceContainment_0_1With(linkTestConcept("substitute"))
155
+ return waitForReceivedMessages(1)
166
156
  case "DeleteContainment_0_1":
167
157
  linkTestConcept().containment_0_1 = undefined
168
- return waitForReceived(1)
158
+ return waitForReceivedMessages(1)
169
159
  case "AddContainment_0_1_Containment_0_1":
170
160
  linkTestConcept().containment_0_1!.containment_0_1 = linkTestConcept("containment_0_1_containment_0_1")
171
- return waitForReceived(1)
161
+ return waitForReceivedMessages(1)
172
162
  case "AddContainment_1_Containment_0_1":
173
163
  linkTestConcept().containment_1.containment_0_1 = linkTestConcept("containment_1_containment_0_1")
174
- return waitForReceived(1)
164
+ return waitForReceivedMessages(1)
175
165
  case "AddContainment_0_n":
176
166
  linkTestConcept().addContainment_0_n(linkTestConcept("containment_0_n_child0")); // (keep ;!)
177
167
  linkTestConcept().addContainment_0_n(linkTestConcept("containment_0_n_child1"))
178
- return waitForReceived(2)
168
+ return waitForReceivedMessages(2)
169
+ case "AddContainment_0_n_Containment_0_n": {
170
+ const outerChild = linkTestConcept("containment_0_n_child0")
171
+ outerChild.addContainment_0_n(linkTestConcept("containment_0_n_containment_0_n_child0"))
172
+ linkTestConcept().addContainment_0_n(outerChild)
173
+ return waitForReceivedMessages(1)
174
+ }
179
175
  case "AddContainment_1_n":
180
176
  linkTestConcept().addContainment_1_n(linkTestConcept("containment_1_n_child0")); // (keep ;!)
181
177
  linkTestConcept().addContainment_1_n(linkTestConcept("containment_1_n_child1"))
182
- return waitForReceived(2)
178
+ return waitForReceivedMessages(2)
183
179
  case "MoveAndReplaceChildFromOtherContainment_Single":
184
180
  linkTestConcept().containment_1.replaceContainment_0_1With(linkTestConcept().containment_0_1!.containment_0_1!)
185
- return waitForReceived(1)
181
+ return waitForReceivedMessages(1)
186
182
  case "MoveAndReplaceChildFromOtherContainmentInSameParent_Single":
187
183
  linkTestConcept().replaceContainment_1With(linkTestConcept().containment_0_1!)
188
- return waitForReceived(1)
184
+ return waitForReceivedMessages(1)
189
185
  case "MoveAndReplaceChildFromOtherContainment_Multiple":
190
- linkTestConcept().replaceContainment_1_nAtIndex(lastOf(linkTestConcept().containment_0_n), linkTestConcept().containment_1_n.length - 1)
191
- return waitForReceived(1)
186
+ if (linkTestConcept().containment_1_n.length === 0) {
187
+ throw new Error(`can't replace an item of an array with no items`)
188
+ }
189
+ linkTestConcept().replaceContainment_1_nAtIndex(
190
+ lastOfArray(lastOfArray(linkTestConcept().containment_0_n).containment_0_n),
191
+ linkTestConcept().containment_1_n.length - 1
192
+ )
193
+ return waitForReceivedMessages(1)
192
194
  case "MoveChildInSameContainment":
193
- linkTestConcept().addContainment_0_nAtIndex(lastOf(linkTestConcept().containment_0_n), 0)
194
- return waitForReceived(1)
195
+ linkTestConcept().addContainment_0_nAtIndex(lastOfArray(linkTestConcept().containment_0_n), 0)
196
+ // Note: this is effectively a move rather than an insert — hence the name of the task.
197
+ return waitForReceivedMessages(1)
195
198
  case "MoveChildFromOtherContainment_Single":
196
199
  linkTestConcept().containment_1 = linkTestConcept().containment_0_1!.containment_0_1!
197
- return waitForReceived(1)
200
+ return waitForReceivedMessages(1)
198
201
  case "MoveChildFromOtherContainment_Multiple":
199
- linkTestConcept().addContainment_1_nAtIndex(lastOf(linkTestConcept().containment_0_n).containment_0_n[0], 1)
200
- return waitForReceived(1)
202
+ linkTestConcept().addContainment_1_nAtIndex(lastOfArray(linkTestConcept().containment_0_n).containment_0_n[0], 1)
203
+ return waitForReceivedMessages(1)
201
204
  case "MoveChildFromOtherContainmentInSameParent_Single":
202
205
  linkTestConcept().containment_1 = linkTestConcept().containment_0_1!
203
- return waitForReceived(1)
206
+ return waitForReceivedMessages(1)
204
207
  case "MoveChildFromOtherContainmentInSameParent_Multiple":
205
- linkTestConcept().addContainment_1_nAtIndex(lastOf(linkTestConcept().containment_0_n), 1)
206
- return waitForReceived(1)
208
+ linkTestConcept().addContainment_1_nAtIndex(lastOfArray(linkTestConcept().containment_0_n), 1)
209
+ return waitForReceivedMessages(1)
207
210
  case "AddPartition":
208
211
  lionWebClient.addPartition(linkTestConcept("partition"))
209
- return waitForReceived(1)
212
+ return waitForReceivedMessages(1)
210
213
 
211
214
  default: {
212
- console.log(withStylesApplied("italic", "red")(`task "${task}" is unknown => ignored`))
215
+ // (shouldn't happen because of upfront validation of tasks)
216
+ console.log(genericWarning(`task "${task}" is unknown => ignored`))
213
217
  return Promise.resolve()
214
218
  }
215
219
  }