@rdfc/js-runner 2.0.0-alpha.9 → 3.0.0

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 (72) hide show
  1. package/.editorconfig +9 -0
  2. package/.github/renovate.json +3 -0
  3. package/README.md +127 -3
  4. package/__tests__/channels.test.ts +131 -74
  5. package/__tests__/echoProcessor.test.ts +131 -0
  6. package/__tests__/testProcessor.test.ts +69 -0
  7. package/eslint.config.mjs +1 -1
  8. package/examples/echo/.idea/echo.iml +9 -0
  9. package/examples/echo/.idea/misc.xml +6 -0
  10. package/{.idea → examples/echo/.idea}/modules.xml +1 -1
  11. package/examples/echo/.idea/vcs.xml +7 -0
  12. package/examples/echo/.swls/config.json +1 -0
  13. package/examples/echo/index.ttl +3 -0
  14. package/examples/echo/minimal.ttl +90 -0
  15. package/examples/echo/shacl.ttl +9 -0
  16. package/examples/echo/shape.ttl +1339 -0
  17. package/examples/echo/test.ttl +11 -0
  18. package/examples/echo/untitled:/types/MyType.ttl +0 -0
  19. package/file:/home/silvius/Projects/mumo-pipeline/ldes/http_3A_2F_2Fdata.mumo.be_2Fstreams_2Fnodes_2Fdefault/root/index.trig +3 -0
  20. package/index.ttl +3 -31
  21. package/ldes/http_3A_2F_2Fdata.mumo.be_2Fstreams_2Fnodes_2Fdefault/root/index.trig +3 -0
  22. package/lib/client.js +7 -10
  23. package/lib/logger.d.ts +2 -2
  24. package/lib/logger.js +3 -3
  25. package/lib/reader.d.ts +8 -6
  26. package/lib/reader.js +135 -26
  27. package/lib/runner.d.ts +10 -5
  28. package/lib/runner.js +86 -46
  29. package/lib/testUtils/duplex.d.ts +25 -0
  30. package/lib/testUtils/duplex.js +70 -0
  31. package/lib/testUtils/index.d.ts +51 -0
  32. package/lib/testUtils/index.js +243 -0
  33. package/lib/testUtils.d.ts +6 -0
  34. package/lib/testUtils.js +92 -2
  35. package/lib/tsconfig.tsbuildinfo +1 -1
  36. package/lib/writer.d.ts +12 -5
  37. package/lib/writer.js +66 -13
  38. package/minimal.ttl +99 -0
  39. package/package.json +5 -5
  40. package/src/client.ts +9 -12
  41. package/src/logger.ts +3 -3
  42. package/src/reader.ts +207 -30
  43. package/src/runner.ts +128 -65
  44. package/src/testUtils/duplex.ts +112 -0
  45. package/src/testUtils/index.ts +430 -0
  46. package/src/writer.ts +106 -16
  47. package/.idea/LNKD.tech Editor.xml +0 -194
  48. package/.idea/codeStyles/Project.xml +0 -52
  49. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  50. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  51. package/.idea/js-runner.iml +0 -12
  52. package/.idea/vcs.xml +0 -6
  53. package/dist/args.d.ts +0 -4
  54. package/dist/args.js +0 -58
  55. package/dist/connectors/file.d.ts +0 -15
  56. package/dist/connectors/file.js +0 -89
  57. package/dist/connectors/http.d.ts +0 -14
  58. package/dist/connectors/http.js +0 -82
  59. package/dist/connectors/kafka.d.ts +0 -48
  60. package/dist/connectors/kafka.js +0 -68
  61. package/dist/connectors/ws.d.ts +0 -10
  62. package/dist/connectors/ws.js +0 -72
  63. package/dist/connectors.d.ts +0 -73
  64. package/dist/connectors.js +0 -168
  65. package/dist/index.cjs +0 -732
  66. package/dist/index.d.ts +0 -42
  67. package/dist/index.js +0 -83
  68. package/dist/tsconfig.tsbuildinfo +0 -1
  69. package/dist/util.d.ts +0 -71
  70. package/dist/util.js +0 -92
  71. package/src/jsonld.ts +0 -220
  72. package/src/testUtils.ts +0 -77
@@ -0,0 +1,430 @@
1
+ import {
2
+ DataChunk,
3
+ FromRunner,
4
+ LogMessage,
5
+ Processor as ProcConfig,
6
+ ToRunner,
7
+ } from '@rdfc/proto'
8
+ import {} from '../reexports'
9
+ import { extractShapes } from 'rdf-lens'
10
+ import { NamedNode, Parser, Writer as N3Writer } from 'n3'
11
+ import { readFile } from 'fs/promises'
12
+ import winston, { createLogger } from 'winston'
13
+ import { Processor } from '../processor'
14
+ import { FullProc, Runner, Writable } from '../runner'
15
+ import { Quad } from '@rdfjs/types'
16
+ import { createTermNamespace } from '@treecg/types'
17
+ import {
18
+ ReceivingStreamControl,
19
+ SendingStreamControl,
20
+ StreamChunk,
21
+ StreamIdentify,
22
+ } from '@rdfc/proto/lib/generated/common'
23
+ import { MockClientDuplexStream } from './duplex'
24
+ import { promisify } from 'util'
25
+ import { Reader } from '../reader'
26
+ import { Writer } from '../writer'
27
+
28
+ export function channel(runner: Runner, name: string): [Writer, Reader] {
29
+ const n = new NamedNode(name)
30
+ const reader = runner.createReader(n)
31
+ const writer = runner.createWriter(n)
32
+
33
+ return [writer, reader]
34
+ }
35
+
36
+ export class StreamMsgMock {
37
+ data: DataChunk[] = []
38
+
39
+ private readonly resolveId: (id: StreamIdentify) => number
40
+
41
+ constructor(resolveId: (id: StreamIdentify) => number) {
42
+ this.resolveId = resolveId
43
+ }
44
+
45
+ sendStreamMessage(): MockClientDuplexStream<
46
+ StreamChunk,
47
+ ReceivingStreamControl
48
+ > {
49
+ const sendingStream = new MockClientDuplexStream<
50
+ StreamChunk,
51
+ ReceivingStreamControl
52
+ >()
53
+
54
+ let at = 0
55
+ sendingStream.register(
56
+ (x) => x.data,
57
+ (d, send) => {
58
+ setTimeout(() => send({ streamSequenceNumber: ++at }), 20)
59
+ this.data.push(d)
60
+ },
61
+ )
62
+ sendingStream.register(
63
+ (x) => x.id,
64
+ (d, send) => send({ streamSequenceNumber: this.resolveId(d) }),
65
+ )
66
+ return sendingStream
67
+ }
68
+ }
69
+
70
+ export class OrchestratorMock {
71
+ connectStream: MockClientDuplexStream<FromRunner, ToRunner>
72
+
73
+ streamMsgs: {
74
+ [id: string]: {
75
+ toProducingStream: (id: ReceivingStreamControl) => void
76
+ receivingStream?: MockClientDuplexStream<SendingStreamControl, DataChunk>
77
+ }
78
+ } = {}
79
+
80
+ streamMsgCount = 0
81
+
82
+ connect(): MockClientDuplexStream<FromRunner, ToRunner> {
83
+ const connectStream = new MockClientDuplexStream<FromRunner, ToRunner>()
84
+ // Always bounce processed msgs back to the runner
85
+ connectStream.register(
86
+ (msg) => msg.processed,
87
+ ({ channel, globalSequenceNumber }, send) => {
88
+ send({
89
+ processed: { channel, localSequenceNumber: globalSequenceNumber },
90
+ })
91
+ },
92
+ )
93
+
94
+ // Always bounce data msgs back to the runner
95
+ connectStream.register(
96
+ (msg) => msg.msg,
97
+ ({ localSequenceNumber, data, channel }, send) => {
98
+ send({
99
+ msg: { globalSequenceNumber: localSequenceNumber, channel, data },
100
+ })
101
+ },
102
+ )
103
+
104
+ // Always bounce close msgs back to the runner
105
+ connectStream.register(
106
+ (msg) => msg.close,
107
+ (close, send) => {
108
+ send({ close })
109
+ },
110
+ )
111
+
112
+ this.connectStream = connectStream
113
+ return connectStream
114
+ }
115
+
116
+ sendStreamMessage(): MockClientDuplexStream<
117
+ StreamChunk,
118
+ ReceivingStreamControl
119
+ > {
120
+ const out = new MockClientDuplexStream<
121
+ StreamChunk,
122
+ ReceivingStreamControl
123
+ >()
124
+ const id = this.streamMsgCount
125
+ this.streamMsgCount++
126
+
127
+ out.registerOnce(
128
+ (x) => x.id,
129
+ ({ channel }, toProducingStream) => {
130
+ this.streamMsgs[id] = { toProducingStream }
131
+
132
+ // Notify stream message
133
+ this.connectStream.send({
134
+ streamMsg: { channel, globalSequenceNumber: id },
135
+ })
136
+ },
137
+ )
138
+
139
+ out.register(
140
+ (x) => x.data,
141
+ (data) => {
142
+ // bounce data to receiving stream
143
+ this.streamMsgs[id].receivingStream!.send(data)
144
+ },
145
+ )
146
+
147
+ out.on('end', () => {
148
+ // end receiving stream
149
+ this.streamMsgs[id].receivingStream!.end()
150
+ })
151
+
152
+ return out
153
+ }
154
+
155
+ receiveStreamMessage(): MockClientDuplexStream<
156
+ SendingStreamControl,
157
+ DataChunk
158
+ > {
159
+ const receivingStream = new MockClientDuplexStream<
160
+ SendingStreamControl,
161
+ DataChunk
162
+ >()
163
+ let streamId = 0
164
+
165
+ receivingStream.registerOnce(
166
+ (x) => x.globalSequenceNumber,
167
+ (id) => {
168
+ streamId = id
169
+ this.streamMsgs[id].receivingStream = receivingStream
170
+ this.streamMsgs[id].toProducingStream({ streamSequenceNumber: 0 })
171
+ },
172
+ )
173
+
174
+ receivingStream.register(
175
+ (x) => x.streamSequenceNumber,
176
+ (streamSequenceNumber) => {
177
+ // Bounce processed message to producing stream
178
+ this.streamMsgs[streamId].toProducingStream({ streamSequenceNumber })
179
+ },
180
+ )
181
+
182
+ return receivingStream
183
+ }
184
+
185
+ logStream(): MockClientDuplexStream<LogMessage, null> {
186
+ const logStream = new MockClientDuplexStream<LogMessage, null>()
187
+ logStream.register((x) => x, console.log)
188
+ return logStream
189
+ }
190
+ }
191
+
192
+ export function createRunner(uri = 'http://example.com/ns#') {
193
+ const logger = createLogger({
194
+ transports: new winston.transports.Console({
195
+ level: process.env['DEBUG'] || 'info',
196
+ }),
197
+ })
198
+
199
+ // Mock the GRPC
200
+ const client = new OrchestratorMock()
201
+
202
+ // Connect just like client.ts:start()
203
+ const stream = client.connect()
204
+ const writable = promisify(stream.write.bind(stream))
205
+
206
+ const runner = new Runner(
207
+ /* eslint-disable @typescript-eslint/no-explicit-any */
208
+ client as any, // Type assertion for testing
209
+ writable as Writable,
210
+ uri,
211
+ logger,
212
+ )
213
+
214
+ stream.on('data', (msg: ToRunner) => runner.handleOrchMessage(msg))
215
+
216
+ return runner
217
+ }
218
+
219
+ export async function one<T>(iter: AsyncIterable<T>): Promise<T | undefined> {
220
+ for await (const item of iter) {
221
+ return item
222
+ }
223
+ }
224
+
225
+ const shapeQuads = `
226
+ @prefix rdfc: <https://w3id.org/rdf-connect#>.
227
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
228
+ @prefix sh: <http://www.w3.org/ns/shacl#>.
229
+ [ ] a sh:NodeShape;
230
+ sh:targetClass <JsProcessorShape>;
231
+ sh:property [
232
+ sh:path rdfc:entrypoint;
233
+ sh:name "location";
234
+ sh:minCount 1;
235
+ sh:maxCount 1;
236
+ sh:datatype xsd:string;
237
+ ], [
238
+ sh:path rdfc:file;
239
+ sh:name "file";
240
+ sh:minCount 1;
241
+ sh:maxCount 1;
242
+ sh:datatype xsd:string;
243
+ ], [
244
+ sh:path rdfc:class;
245
+ sh:name "clazz";
246
+ sh:maxCount 1;
247
+ sh:datatype xsd:string;
248
+ ].
249
+ `
250
+ export type ConfigType = {
251
+ location: string
252
+ file: string
253
+ clazz: string
254
+ }
255
+
256
+ const OWL = createTermNamespace('http://www.w3.org/2002/07/owl#', 'imports')
257
+ const processorShapes = extractShapes(new Parser().parse(shapeQuads))
258
+ const base = 'https://w3id.org/rdf-connect#'
259
+
260
+ export async function importFile(
261
+ file: string,
262
+ content?: string,
263
+ ): Promise<Quad[]> {
264
+ const done = new Set<string>()
265
+
266
+ const quads: Quad[] = []
267
+ const todo: URL[] = []
268
+
269
+ const parse = (content: string, baseIRI: URL) => {
270
+ done.add(baseIRI.toString())
271
+
272
+ const extras = new Parser({ baseIRI: baseIRI.toString() }).parse(content)
273
+
274
+ for (const o of extras
275
+ .filter(
276
+ (x) =>
277
+ x.subject.value === baseIRI?.toString() &&
278
+ x.predicate.equals(OWL.imports),
279
+ )
280
+ .map((x) => x.object.value)) {
281
+ todo.push(new URL(o))
282
+ }
283
+
284
+ quads.push(...extras)
285
+ }
286
+
287
+ if (content) {
288
+ parse(content, new URL('file://' + file))
289
+ } else {
290
+ todo.push(new URL('file://' + file))
291
+ }
292
+
293
+ let item = todo.pop()
294
+ while (item !== undefined) {
295
+ if (done.has(item.toString())) {
296
+ item = todo.pop()
297
+ continue
298
+ }
299
+
300
+ if (item.protocol !== 'file:') {
301
+ throw 'No supported protocol ' + item.protocol
302
+ }
303
+ const txt = await readFile(item.pathname, { encoding: 'utf8' })
304
+ parse(txt, item)
305
+
306
+ item = todo.pop()
307
+ }
308
+
309
+ return quads
310
+ }
311
+
312
+ /**
313
+ * Helper class to gradually test your processors.
314
+ * Possible flow:
315
+ * - import the JsRunner index file
316
+ * - import your processor config file
317
+ * - test if the config is as you would expect (from getConfig())
318
+ * - import your processor definition (inline)
319
+ * - build your processor
320
+ * - test your processor
321
+ */
322
+ export class ProcHelper<T extends Processor<unknown>> {
323
+ runner: Runner
324
+ quads: Quad[] = []
325
+ config: ConfigType
326
+ proc: FullProc<T>
327
+
328
+ constructor(uri?: string) {
329
+ this.runner = createRunner(uri)
330
+ }
331
+
332
+ async importInline(baseIRI: string, config: string) {
333
+ const configQuads = await importFile(baseIRI, config)
334
+ this.quads.push(...configQuads)
335
+ }
336
+
337
+ async importFile(file: string) {
338
+ const configQuads = await importFile(file)
339
+ this.quads.push(...configQuads)
340
+ }
341
+
342
+ getConfig(ty: string | NamedNode): ConfigType {
343
+ const id = typeof ty === 'string' ? new NamedNode(base + ty) : ty
344
+ const procConfig = <ConfigType>processorShapes.lenses[
345
+ 'JsProcessorShape'
346
+ ].execute({
347
+ id,
348
+ quads: this.quads,
349
+ })
350
+
351
+ this.config = procConfig
352
+ return procConfig
353
+ }
354
+
355
+ async getProcessor(
356
+ uri: string = 'http://example.com/ns#processor',
357
+ ): Promise<FullProc<T>> {
358
+ await this.runner.handleOrchMessage({
359
+ pipeline: new N3Writer().quadsToString(this.quads),
360
+ })
361
+
362
+ const proc = await this.runner.addProcessor<T>({
363
+ config: JSON.stringify(this.config),
364
+ arguments: '',
365
+ uri,
366
+ })
367
+ return proc
368
+ }
369
+ }
370
+
371
+ /**
372
+ * @deprecated use {@link ProcHelper}
373
+ */
374
+ export async function getProcInline<T extends Processor<unknown>>(
375
+ config: string,
376
+ ty: string,
377
+ runner: Runner,
378
+ baseIRI: string,
379
+ uri = 'http://example.com/ns#processor',
380
+ ): Promise<FullProc<T>> {
381
+ const configQuads = await importFile(baseIRI, config)
382
+ const procConfig = <ProcConfig>processorShapes.lenses[
383
+ 'JsProcessorShape'
384
+ ].execute({
385
+ id: new NamedNode(base + ty),
386
+ quads: configQuads,
387
+ })
388
+
389
+ await runner.handleOrchMessage({
390
+ pipeline: new N3Writer().quadsToString(configQuads),
391
+ })
392
+
393
+ const proc = await runner.addProcessor<T>({
394
+ config: JSON.stringify(procConfig),
395
+ arguments: '',
396
+ uri,
397
+ })
398
+
399
+ return proc
400
+ }
401
+
402
+ /**
403
+ * @deprecated use {@link ProcHelper}
404
+ */
405
+ export async function getProc<T extends Processor<unknown>>(
406
+ config: string,
407
+ ty: string,
408
+ runner: Runner,
409
+ configLocation: string,
410
+ uri = 'http://example.com/ns#processor',
411
+ ): Promise<FullProc<T>> {
412
+ const configQuads = await importFile(configLocation)
413
+ const procConfig = processorShapes.lenses['JsProcessorShape'].execute({
414
+ id: new NamedNode(base + ty),
415
+ quads: configQuads,
416
+ })
417
+
418
+ configQuads.push(...new Parser().parse(config))
419
+ await runner.handleOrchMessage({
420
+ pipeline: new N3Writer().quadsToString(configQuads),
421
+ })
422
+
423
+ const proc = await runner.addProcessor<T>({
424
+ config: JSON.stringify(procConfig),
425
+ arguments: '',
426
+ uri,
427
+ })
428
+
429
+ return proc
430
+ }
package/src/writer.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { Id, OrchestratorMessage, RunnerClient } from '@rdfc/proto'
1
+ import { FromRunner, RunnerClient } from '@rdfc/proto'
2
2
  import { promisify } from 'util'
3
3
  import { Logger } from 'winston'
4
4
  import { Any } from './reader'
5
5
 
6
- type Writable = (msg: OrchestratorMessage) => Promise<unknown>
6
+ type Writable = (msg: FromRunner) => Promise<unknown>
7
7
  export interface Writer {
8
8
  readonly uri: string
9
9
  buffer(buffer: Uint8Array): Promise<void>
@@ -11,7 +11,7 @@ export interface Writer {
11
11
  stream(buffer: AsyncIterable<Uint8Array>): Promise<void>
12
12
  stream<T>(
13
13
  buffer: AsyncIterable<T>,
14
- tranform: (x: T) => Uint8Array,
14
+ transform: (x: T) => Uint8Array,
15
15
  ): Promise<void>
16
16
 
17
17
  string(buffer: string): Promise<void>
@@ -21,21 +21,36 @@ export interface Writer {
21
21
  const encoder = new TextEncoder()
22
22
  export class WriterInstance implements Writer {
23
23
  readonly uri: string
24
+ localSequenceNumber: number = 1
24
25
  private readonly client: RunnerClient
25
- private readonly write: Writable
26
+ private readonly notifyOrchestrator: Writable
26
27
  private readonly logger: Logger
27
28
 
29
+ private awaitingProcessed: Array<() => void> = []
30
+
31
+ private openStreams: number = 0
32
+ private shouldClose: Array<() => void> = []
33
+
34
+ private readonly runnerId: string
35
+
28
36
  constructor(
29
37
  uri: string,
30
38
  client: RunnerClient,
31
- write: Writable,
39
+ notifyOrchestrator: Writable,
40
+ runnerId: string,
32
41
  logger: Logger,
33
42
  ) {
34
43
  this.client = client
35
- this.write = write
44
+ this.notifyOrchestrator = notifyOrchestrator
36
45
  this.uri = uri
37
46
  this.logger = logger
47
+ this.runnerId = runnerId
38
48
  }
49
+
50
+ private awaitProcessed(): Promise<void> {
51
+ return new Promise((res) => this.awaitingProcessed.push(res))
52
+ }
53
+
39
54
  async any(any: Any): Promise<void> {
40
55
  if ('stream' in any) {
41
56
  await this.stream(any.stream)
@@ -50,41 +65,116 @@ export class WriterInstance implements Writer {
50
65
 
51
66
  async buffer(buffer: Uint8Array): Promise<void> {
52
67
  this.logger.debug(`${this.uri} sends buffer ${buffer.length} bytes`)
53
- await this.write({ msg: { data: buffer, channel: this.uri } })
68
+ const localSequenceNumber = this.localSequenceNumber++
69
+ const handledPromise = this.awaitProcessed()
70
+
71
+ await this.notifyOrchestrator({
72
+ msg: { data: buffer, channel: this.uri, localSequenceNumber },
73
+ })
74
+ await handledPromise
54
75
  }
55
76
 
56
77
  async stream<T = Uint8Array>(
57
78
  buffer: AsyncIterable<T>,
58
79
  transform?: (x: T) => Uint8Array,
59
80
  ) {
81
+ this.openStreams += 1
60
82
  const t = transform || ((x: unknown) => <Uint8Array>x)
61
83
  const stream = this.client.sendStreamMessage()
62
- const id: Id = await new Promise((res) => stream.once('data', res))
63
- this.logger.debug(`${this.uri} streams message with id ${id.id}`)
64
- await this.write({ streamMsg: { id, channel: this.uri } })
65
84
 
66
- const write = promisify(stream.write.bind(stream))
85
+ const handledPromise = this.awaitProcessed()
86
+ const writeStreamMessageChunk = promisify(stream.write.bind(stream))
87
+ const localSequenceNumber = this.localSequenceNumber++
88
+ await writeStreamMessageChunk({
89
+ id: {
90
+ channel: this.uri,
91
+ localSequenceNumber,
92
+ runner: this.runnerId,
93
+ },
94
+ })
95
+
96
+ const id = await new Promise((res) => stream.once('data', res))
97
+
98
+ this.logger.debug(
99
+ `${this.uri} streams message with id ${JSON.stringify(id)}`,
100
+ )
101
+
67
102
  for await (const msg of buffer) {
68
- await write({ data: t(msg) })
103
+ const processedPromise = new Promise((res) => stream.once('data', res))
104
+ await writeStreamMessageChunk({ data: { data: t(msg) } })
105
+ // Await a message on the stream, indicating that the chunk has been processed
106
+ await processedPromise
69
107
  }
70
108
 
71
- this.logger.debug(`${this.uri} is done streaming message with id ${id.id}`)
72
109
  stream.end()
110
+
111
+ await handledPromise
112
+
113
+ this.openStreams -= 1
114
+
115
+ if (this.shouldClose.length > 0) await this.close()
73
116
  }
74
117
 
75
118
  async string(msg: string): Promise<void> {
76
119
  this.logger.debug(`${this.uri} sends string ${msg.length} characters`)
77
- await this.write({
78
- msg: { data: encoder.encode(msg), channel: this.uri },
120
+ const localSequenceNumber = this.localSequenceNumber++
121
+ const handledPromise = this.awaitProcessed()
122
+
123
+ await this.notifyOrchestrator({
124
+ msg: {
125
+ data: encoder.encode(msg),
126
+ channel: this.uri,
127
+ localSequenceNumber,
128
+ },
79
129
  })
130
+
131
+ await handledPromise
80
132
  }
81
133
 
134
+ /**
135
+ * Gracefully closes this channel.
136
+ *
137
+ * Behavior:
138
+ * - If there are still active streams, closing is deferred until they complete.
139
+ * - If multiple callers invoke `close()` while waiting, their Promises are queued and
140
+ * resolved once the channel actually closes.
141
+ * - If this side initiated the close (`issued = false`), a close message is sent to the remote.
142
+ *
143
+ * @param issued - If true, indicates the close request originated remotely
144
+ */
82
145
  async close(issued = false): Promise<void> {
146
+ // Case 1: Active streams still running → wait until they finish
147
+ if (this.openStreams !== 0) {
148
+ await new Promise<void>((resolve) => this.shouldClose.push(resolve))
149
+ return
150
+ }
151
+
152
+ // Case 2: No active streams → perform actual close
83
153
  this.logger.debug(`${this.uri} closes stream`)
84
154
  if (!issued) {
85
- await this.write({
155
+ await this.notifyOrchestrator({
86
156
  close: { channel: this.uri },
87
157
  })
88
158
  }
159
+
160
+ let resolve = this.shouldClose.pop()
161
+ while (resolve) {
162
+ resolve()
163
+ resolve = this.shouldClose.pop()
164
+ }
165
+ }
166
+
167
+ /**
168
+ * A message is handled, let's notify the fifo {@link awaitProcessed}
169
+ */
170
+ handled(): void {
171
+ if (this.awaitingProcessed.length > 0) {
172
+ this.awaitingProcessed.shift()!()
173
+ } else {
174
+ this.logger.error(
175
+ 'Expected to be waiting for a message to be processed, but this is not the case ' +
176
+ this.uri,
177
+ )
178
+ }
89
179
  }
90
180
  }