@rdfc/js-runner 2.0.0 → 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.
- package/.editorconfig +9 -0
- package/.github/renovate.json +3 -0
- package/README.md +127 -3
- package/__tests__/channels.test.ts +131 -74
- package/__tests__/echoProcessor.test.ts +131 -0
- package/__tests__/testProcessor.test.ts +69 -0
- package/eslint.config.mjs +1 -1
- package/examples/echo/.idea/echo.iml +9 -0
- package/examples/echo/.idea/misc.xml +6 -0
- package/{.idea → examples/echo/.idea}/modules.xml +1 -1
- package/examples/echo/.idea/vcs.xml +7 -0
- package/examples/echo/.swls/config.json +1 -0
- package/examples/echo/index.ttl +3 -0
- package/examples/echo/minimal.ttl +90 -0
- package/examples/echo/shacl.ttl +9 -0
- package/examples/echo/shape.ttl +1339 -0
- package/examples/echo/test.ttl +11 -0
- package/examples/echo/untitled:/types/MyType.ttl +0 -0
- package/file:/home/silvius/Projects/mumo-pipeline/ldes/http_3A_2F_2Fdata.mumo.be_2Fstreams_2Fnodes_2Fdefault/root/index.trig +3 -0
- package/index.ttl +3 -31
- package/ldes/http_3A_2F_2Fdata.mumo.be_2Fstreams_2Fnodes_2Fdefault/root/index.trig +3 -0
- package/lib/client.js +6 -9
- package/lib/logger.d.ts +2 -2
- package/lib/logger.js +3 -3
- package/lib/reader.d.ts +8 -6
- package/lib/reader.js +135 -25
- package/lib/runner.d.ts +10 -5
- package/lib/runner.js +86 -46
- package/lib/testUtils/duplex.d.ts +25 -0
- package/lib/testUtils/duplex.js +70 -0
- package/lib/testUtils/index.d.ts +51 -0
- package/lib/testUtils/index.js +243 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/writer.d.ts +12 -5
- package/lib/writer.js +66 -13
- package/minimal.ttl +99 -0
- package/package.json +3 -3
- package/src/client.ts +8 -11
- package/src/logger.ts +3 -3
- package/src/reader.ts +207 -29
- package/src/runner.ts +128 -65
- package/src/testUtils/duplex.ts +112 -0
- package/src/testUtils/index.ts +430 -0
- package/src/writer.ts +106 -16
- package/.idea/LNKD.tech Editor.xml +0 -194
- package/.idea/codeStyles/Project.xml +0 -52
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/js-runner.iml +0 -12
- package/.idea/vcs.xml +0 -6
- package/dist/args.d.ts +0 -4
- package/dist/args.js +0 -58
- package/dist/connectors/file.d.ts +0 -15
- package/dist/connectors/file.js +0 -89
- package/dist/connectors/http.d.ts +0 -14
- package/dist/connectors/http.js +0 -82
- package/dist/connectors/kafka.d.ts +0 -48
- package/dist/connectors/kafka.js +0 -68
- package/dist/connectors/ws.d.ts +0 -10
- package/dist/connectors/ws.js +0 -72
- package/dist/connectors.d.ts +0 -73
- package/dist/connectors.js +0 -168
- package/dist/index.cjs +0 -732
- package/dist/index.d.ts +0 -42
- package/dist/index.js +0 -83
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/util.d.ts +0 -71
- package/dist/util.js +0 -92
- package/src/jsonld.ts +0 -220
- package/src/testUtils.ts +0 -196
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
39
|
+
notifyOrchestrator: Writable,
|
|
40
|
+
runnerId: string,
|
|
32
41
|
logger: Logger,
|
|
33
42
|
) {
|
|
34
43
|
this.client = client
|
|
35
|
-
this.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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.
|
|
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
|
}
|