@rdfc/js-runner 3.0.3 → 3.1.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/.idea/LNKD.tech Editor.xml +194 -0
- package/.idea/codeStyles/Project.xml +52 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/copilot.data.migration.agent.xml +6 -0
- package/.idea/copilot.data.migration.ask.xml +6 -0
- package/.idea/copilot.data.migration.ask2agent.xml +6 -0
- package/.idea/copilot.data.migration.edit.xml +6 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/js-runner.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/__tests__/channels.test.ts +70 -1
- package/lib/reader.d.ts +4 -0
- package/lib/reader.js +47 -1
- package/lib/runner.js +2 -2
- package/lib/testUtils/index.js +7 -3
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/writer.d.ts +14 -3
- package/lib/writer.js +83 -26
- package/package.json +12 -12
- package/src/reader.ts +56 -0
- package/src/runner.ts +1 -1
- package/src/testUtils/index.ts +6 -2
- package/src/writer.ts +175 -42
package/src/writer.ts
CHANGED
|
@@ -4,19 +4,79 @@ import { Logger } from 'winston'
|
|
|
4
4
|
import { Any } from './reader'
|
|
5
5
|
|
|
6
6
|
type Writable = (msg: FromRunner) => Promise<unknown>
|
|
7
|
+
export type Handler<T = void> = [T] extends [void]
|
|
8
|
+
? () => void | Promise<void>
|
|
9
|
+
: (value: T) => void | Promise<void>
|
|
10
|
+
|
|
7
11
|
export interface Writer {
|
|
8
12
|
readonly uri: string
|
|
13
|
+
readonly canceled: boolean
|
|
14
|
+
on(event: 'cancel', listener: Handler): this
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Writes a complete buffer to the channel. The Promise resolves once the message is fully processed by the remote.
|
|
18
|
+
*
|
|
19
|
+
* @throws Error if the channel is closed or canceled at the moment of the write operation.
|
|
20
|
+
* @param buffer - The data to send as a Uint8Array
|
|
21
|
+
* @returns A Promise that resolves when the message is acknowledged as processed by the remote.
|
|
22
|
+
*/
|
|
9
23
|
buffer(buffer: Uint8Array): Promise<void>
|
|
10
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Writes a stream of data to a separate stream-specific channel.
|
|
27
|
+
* The Promise resolves once the entire stream is fully processed by the remote.
|
|
28
|
+
*
|
|
29
|
+
* @throws Error if the channel is closed or canceled at the moment of initiating a stream-specific channel.
|
|
30
|
+
* @param buffer - An AsyncIterable that produces the data to send as Uint8Arrays
|
|
31
|
+
* @returns A Promise that resolves when the entire stream is acknowledged as processed by the remote.
|
|
32
|
+
*/
|
|
11
33
|
stream(buffer: AsyncIterable<Uint8Array>): Promise<void>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Writes a stream of data to a separate stream-specific channel.
|
|
37
|
+
* The Promise resolves once the entire stream is fully processed by the remote.
|
|
38
|
+
*
|
|
39
|
+
* @throws Error if the channel is closed or canceled at the moment of initiating a stream-specific channel.
|
|
40
|
+
* @param buffer - An AsyncIterable that produces the data to send, which will be transformed into Uint8Arrays using the provided transform function
|
|
41
|
+
* @param transform - A function that transforms items from the buffer AsyncIterable into Uint8Arrays for sending. If not provided, items are assumed to already be Uint8Arrays.
|
|
42
|
+
* @returns A Promise that resolves when the entire stream is acknowledged as processed by the remote.
|
|
43
|
+
*/
|
|
12
44
|
stream<T>(
|
|
13
45
|
buffer: AsyncIterable<T>,
|
|
14
46
|
transform: (x: T) => Uint8Array,
|
|
15
47
|
): Promise<void>
|
|
16
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Writes a string message to the channel. The Promise resolves once the message is fully processed by the remote.
|
|
51
|
+
*
|
|
52
|
+
* @throws Error if the channel is closed or canceled at the moment of the write operation.
|
|
53
|
+
* @param buffer - The string message to send
|
|
54
|
+
* @returns A Promise that resolves when the message is acknowledged as processed by the remote.
|
|
55
|
+
*/
|
|
17
56
|
string(buffer: string): Promise<void>
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Writes a message of any supported type (string, buffer, or stream) to the channel.
|
|
60
|
+
* The Promise resolves once the message is fully processed by the remote.
|
|
61
|
+
*
|
|
62
|
+
* @throws Error if the channel is closed or canceled at the moment of the write operation.
|
|
63
|
+
* @param any - An object containing one of the supported message types (string, buffer, or stream)
|
|
64
|
+
* @returns A Promise that resolves when the message is acknowledged as processed by the remote.
|
|
65
|
+
*/
|
|
18
66
|
any(any: Any): Promise<void>
|
|
19
|
-
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Gracefully closes this channel.
|
|
70
|
+
*
|
|
71
|
+
* Behavior:
|
|
72
|
+
* - If there are still active streams, closing is deferred until they complete.
|
|
73
|
+
* - If multiple callers invoke `close()` while waiting, their Promises are queued and
|
|
74
|
+
* resolved once the channel actually closes.
|
|
75
|
+
* - If this side initiated the close (`issued = false`), a close message is sent to the remote.
|
|
76
|
+
*
|
|
77
|
+
* @param issued - If true, indicates the close request originated remotely
|
|
78
|
+
*/
|
|
79
|
+
close(issued?: boolean): Promise<void>
|
|
20
80
|
}
|
|
21
81
|
const encoder = new TextEncoder()
|
|
22
82
|
export class WriterInstance implements Writer {
|
|
@@ -26,10 +86,20 @@ export class WriterInstance implements Writer {
|
|
|
26
86
|
private readonly notifyOrchestrator: Writable
|
|
27
87
|
private readonly logger: Logger
|
|
28
88
|
|
|
29
|
-
|
|
89
|
+
// FIFO of message-level acknowledgements coming back from the orchestrator.
|
|
90
|
+
private awaitingProcessed: Array<{
|
|
91
|
+
resolve: () => void
|
|
92
|
+
reject: (reason: Error) => void
|
|
93
|
+
}> = []
|
|
30
94
|
|
|
31
95
|
private openStreams: number = 0
|
|
96
|
+
// Close callers wait here while active streams are still flushing.
|
|
32
97
|
private shouldClose: Array<() => void> = []
|
|
98
|
+
private closed = false
|
|
99
|
+
private _canceled = false
|
|
100
|
+
|
|
101
|
+
// Processors can subscribe here to stop upstream work when downstream cancels.
|
|
102
|
+
private readonly cancelHandlers = new Set<Handler>()
|
|
33
103
|
|
|
34
104
|
private readonly runnerId: string
|
|
35
105
|
|
|
@@ -47,8 +117,22 @@ export class WriterInstance implements Writer {
|
|
|
47
117
|
this.runnerId = runnerId
|
|
48
118
|
}
|
|
49
119
|
|
|
50
|
-
|
|
51
|
-
return
|
|
120
|
+
get canceled(): boolean {
|
|
121
|
+
return this._canceled
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
on(event: 'cancel', listener: Handler): this {
|
|
125
|
+
if (event === 'cancel') {
|
|
126
|
+
this.cancelHandlers.add(listener)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return this
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private cancellationError(): Error {
|
|
133
|
+
return new Error(
|
|
134
|
+
`Writer for channel ${this.uri} was canceled by the connected reader`,
|
|
135
|
+
)
|
|
52
136
|
}
|
|
53
137
|
|
|
54
138
|
async any(any: Any): Promise<void> {
|
|
@@ -63,7 +147,18 @@ export class WriterInstance implements Writer {
|
|
|
63
147
|
}
|
|
64
148
|
}
|
|
65
149
|
|
|
150
|
+
private assertCanWrite() {
|
|
151
|
+
if (this._canceled) {
|
|
152
|
+
throw this.cancellationError()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (this.closed) {
|
|
156
|
+
throw new Error(`Writer for channel ${this.uri} is closed`)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
66
160
|
async buffer(buffer: Uint8Array): Promise<void> {
|
|
161
|
+
this.assertCanWrite()
|
|
67
162
|
this.logger.debug(`${this.uri} sends buffer ${buffer.length} bytes`)
|
|
68
163
|
const localSequenceNumber = this.localSequenceNumber++
|
|
69
164
|
const handledPromise = this.awaitProcessed()
|
|
@@ -77,45 +172,59 @@ export class WriterInstance implements Writer {
|
|
|
77
172
|
async stream<T = Uint8Array>(
|
|
78
173
|
buffer: AsyncIterable<T>,
|
|
79
174
|
transform?: (x: T) => Uint8Array,
|
|
80
|
-
) {
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
this.assertCanWrite()
|
|
81
177
|
this.openStreams += 1
|
|
82
178
|
const t = transform || ((x: unknown) => <Uint8Array>x)
|
|
83
179
|
const stream = this.client.sendStreamMessage()
|
|
84
180
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
181
|
+
try {
|
|
182
|
+
// Message-level ack that signals the whole stream message is fully handled.
|
|
183
|
+
const handledPromise = this.awaitProcessed()
|
|
184
|
+
const writeStreamMessageChunk = promisify(stream.write.bind(stream))
|
|
185
|
+
const localSequenceNumber = this.localSequenceNumber++
|
|
186
|
+
await writeStreamMessageChunk({
|
|
187
|
+
id: {
|
|
188
|
+
channel: this.uri,
|
|
189
|
+
localSequenceNumber,
|
|
190
|
+
runner: this.runnerId,
|
|
191
|
+
},
|
|
192
|
+
})
|
|
95
193
|
|
|
96
|
-
|
|
194
|
+
// First response confirms stream id registration on the remote side.
|
|
195
|
+
const id = await new Promise((res) => stream.once('data', res))
|
|
97
196
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
197
|
+
this.logger.debug(
|
|
198
|
+
`${this.uri} streams message with id ${JSON.stringify(id)}`,
|
|
199
|
+
)
|
|
101
200
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
201
|
+
// TODO: don't await to allow consuming processors to read and handle in parallel.
|
|
202
|
+
for await (const msg of buffer) {
|
|
203
|
+
const processedPromise = new Promise((res) => stream.once('data', res))
|
|
204
|
+
await writeStreamMessageChunk({ data: { data: t(msg) } })
|
|
205
|
+
// Await a message on the stream, indicating that the chunk has been processed
|
|
206
|
+
await processedPromise
|
|
207
|
+
}
|
|
108
208
|
|
|
109
|
-
|
|
209
|
+
stream.end()
|
|
110
210
|
|
|
111
|
-
|
|
211
|
+
await handledPromise
|
|
212
|
+
} finally {
|
|
213
|
+
this.openStreams -= 1
|
|
112
214
|
|
|
113
|
-
|
|
215
|
+
if (!stream.writableEnded) {
|
|
216
|
+
stream.end()
|
|
217
|
+
}
|
|
114
218
|
|
|
115
|
-
|
|
219
|
+
// If a close call was deferred while streaming, complete it now.
|
|
220
|
+
if (this.shouldClose.length > 0) {
|
|
221
|
+
await this.close()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
116
224
|
}
|
|
117
225
|
|
|
118
226
|
async string(msg: string): Promise<void> {
|
|
227
|
+
this.assertCanWrite()
|
|
119
228
|
this.logger.debug(`${this.uri} sends string ${msg.length} characters`)
|
|
120
229
|
const localSequenceNumber = this.localSequenceNumber++
|
|
121
230
|
const handledPromise = this.awaitProcessed()
|
|
@@ -131,18 +240,15 @@ export class WriterInstance implements Writer {
|
|
|
131
240
|
await handledPromise
|
|
132
241
|
}
|
|
133
242
|
|
|
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
|
-
*/
|
|
145
243
|
async close(issued = false): Promise<void> {
|
|
244
|
+
this.closed = true
|
|
245
|
+
if (issued && !this._canceled) {
|
|
246
|
+
// Remote initiated close: mark writer canceled to fail future writes.
|
|
247
|
+
this._canceled = true
|
|
248
|
+
|
|
249
|
+
// Notify processors so they can stop producing upstream work as well.
|
|
250
|
+
await this.emitCancel()
|
|
251
|
+
}
|
|
146
252
|
// Case 1: Active streams still running → wait until they finish
|
|
147
253
|
if (this.openStreams !== 0) {
|
|
148
254
|
await new Promise<void>((resolve) => this.shouldClose.push(resolve))
|
|
@@ -167,9 +273,16 @@ export class WriterInstance implements Writer {
|
|
|
167
273
|
/**
|
|
168
274
|
* A message is handled, let's notify the fifo {@link awaitProcessed}
|
|
169
275
|
*/
|
|
170
|
-
handled(): void {
|
|
276
|
+
handled(error?: string): void {
|
|
171
277
|
if (this.awaitingProcessed.length > 0) {
|
|
172
|
-
|
|
278
|
+
if (error) {
|
|
279
|
+
this.awaitingProcessed.shift()!.reject(new Error(error))
|
|
280
|
+
} else {
|
|
281
|
+
this.awaitingProcessed.shift()!.resolve()
|
|
282
|
+
}
|
|
283
|
+
} else if (this.closed || this._canceled) {
|
|
284
|
+
// A late ack can arrive after a close/cancel race; nothing to resolve anymore.
|
|
285
|
+
return
|
|
173
286
|
} else {
|
|
174
287
|
this.logger.error(
|
|
175
288
|
'Expected to be waiting for a message to be processed, but this is not the case ' +
|
|
@@ -177,4 +290,24 @@ export class WriterInstance implements Writer {
|
|
|
177
290
|
)
|
|
178
291
|
}
|
|
179
292
|
}
|
|
293
|
+
|
|
294
|
+
private async emitCancel() {
|
|
295
|
+
await Promise.all(
|
|
296
|
+
Array.from(this.cancelHandlers).map(async (handler) => {
|
|
297
|
+
try {
|
|
298
|
+
await handler()
|
|
299
|
+
} catch (error: unknown) {
|
|
300
|
+
this.logger.error(
|
|
301
|
+
`Cancel listener for channel ${this.uri} failed: ${String(error)}`,
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
}),
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private awaitProcessed(): Promise<void> {
|
|
309
|
+
return new Promise((resolve, reject) =>
|
|
310
|
+
this.awaitingProcessed.push({ resolve, reject }),
|
|
311
|
+
)
|
|
312
|
+
}
|
|
180
313
|
}
|