@rdfc/js-runner 3.0.2 → 3.1.0-alpha.1
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/README.md +4 -0
- package/__tests__/channels.test.ts +66 -1
- package/__tests__/echoProcessor.test.ts +11 -12
- package/bun.lock +820 -0
- package/lib/logger.d.ts +1 -0
- package/lib/logger.js +19 -9
- package/lib/reader.d.ts +4 -0
- package/lib/reader.js +46 -1
- package/lib/runner.js +1 -1
- package/lib/testUtils/duplex.js +1 -1
- package/lib/testUtils/index.d.ts +9 -9
- package/lib/testUtils/index.js +9 -9
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/writer.d.ts +14 -0
- package/lib/writer.js +120 -30
- package/package.json +20 -19
- package/src/logger.ts +15 -7
- package/src/reader.ts +62 -8
- package/src/runner.ts +3 -1
- package/src/testUtils/duplex.ts +1 -4
- package/src/testUtils/index.ts +25 -21
- package/src/writer.ts +172 -32
- package/jest.config.js +0 -2
package/src/writer.ts
CHANGED
|
@@ -4,8 +4,14 @@ 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
|
|
9
15
|
buffer(buffer: Uint8Array): Promise<void>
|
|
10
16
|
|
|
11
17
|
stream(buffer: AsyncIterable<Uint8Array>): Promise<void>
|
|
@@ -26,10 +32,22 @@ export class WriterInstance implements Writer {
|
|
|
26
32
|
private readonly notifyOrchestrator: Writable
|
|
27
33
|
private readonly logger: Logger
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
// FIFO of message-level acknowledgements coming back from the orchestrator.
|
|
36
|
+
private awaitingProcessed: Array<{
|
|
37
|
+
resolve: () => void
|
|
38
|
+
reject: (reason: Error) => void
|
|
39
|
+
}> = []
|
|
30
40
|
|
|
31
41
|
private openStreams: number = 0
|
|
42
|
+
// Close callers wait here while active streams are still flushing.
|
|
32
43
|
private shouldClose: Array<() => void> = []
|
|
44
|
+
private closed = false
|
|
45
|
+
private _canceled = false
|
|
46
|
+
|
|
47
|
+
// Shared cancellation signal to abort in-flight waits when the remote closes.
|
|
48
|
+
private readonly cancelSignal = new AbortController()
|
|
49
|
+
// Processors can subscribe here to stop upstream work when downstream cancels.
|
|
50
|
+
private readonly cancelHandlers = new Set<Handler>()
|
|
33
51
|
|
|
34
52
|
private readonly runnerId: string
|
|
35
53
|
|
|
@@ -47,8 +65,85 @@ export class WriterInstance implements Writer {
|
|
|
47
65
|
this.runnerId = runnerId
|
|
48
66
|
}
|
|
49
67
|
|
|
68
|
+
get canceled(): boolean {
|
|
69
|
+
return this._canceled
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
on(event: 'cancel', listener: Handler): this {
|
|
73
|
+
if (event === 'cancel') {
|
|
74
|
+
this.cancelHandlers.add(listener)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return this
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private cancellationError(): Error {
|
|
81
|
+
return new Error(
|
|
82
|
+
`Writer for channel ${this.uri} was canceled by the connected reader`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private emitCancel() {
|
|
87
|
+
for (const handler of this.cancelHandlers) {
|
|
88
|
+
try {
|
|
89
|
+
Promise.resolve(handler()).catch((error: unknown) => {
|
|
90
|
+
this.logger.error(
|
|
91
|
+
`Cancel listener for channel ${this.uri} failed: ${String(error)}`,
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
} catch (error: unknown) {
|
|
95
|
+
this.logger.error(
|
|
96
|
+
`Cancel listener for channel ${this.uri} failed: ${String(error)}`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private assertCanWrite() {
|
|
103
|
+
if (this._canceled) {
|
|
104
|
+
throw this.cancellationError()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.closed) {
|
|
108
|
+
throw new Error(`Writer for channel ${this.uri} is closed`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async raceWithCancellation<T>(promise: Promise<T>): Promise<T> {
|
|
113
|
+
this.assertCanWrite()
|
|
114
|
+
|
|
115
|
+
return await new Promise<T>((resolve, reject) => {
|
|
116
|
+
const onAbort = () => reject(this.cancellationError())
|
|
117
|
+
this.cancelSignal.signal.addEventListener('abort', onAbort, {
|
|
118
|
+
once: true,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
promise.then(
|
|
122
|
+
(value) => {
|
|
123
|
+
// Clean up the abort listener if the original operation finished first.
|
|
124
|
+
this.cancelSignal.signal.removeEventListener('abort', onAbort)
|
|
125
|
+
resolve(value)
|
|
126
|
+
},
|
|
127
|
+
(error) => {
|
|
128
|
+
this.cancelSignal.signal.removeEventListener('abort', onAbort)
|
|
129
|
+
reject(error)
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private rejectPendingProcessed(error: Error) {
|
|
136
|
+
// Reject all queued message waits so callers do not hang during cancellation.
|
|
137
|
+
while (this.awaitingProcessed.length > 0) {
|
|
138
|
+
this.awaitingProcessed.shift()!.reject(error)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
50
142
|
private awaitProcessed(): Promise<void> {
|
|
51
|
-
|
|
143
|
+
this.assertCanWrite()
|
|
144
|
+
return new Promise((resolve, reject) =>
|
|
145
|
+
this.awaitingProcessed.push({ resolve, reject }),
|
|
146
|
+
)
|
|
52
147
|
}
|
|
53
148
|
|
|
54
149
|
async any(any: Any): Promise<void> {
|
|
@@ -64,6 +159,7 @@ export class WriterInstance implements Writer {
|
|
|
64
159
|
}
|
|
65
160
|
|
|
66
161
|
async buffer(buffer: Uint8Array): Promise<void> {
|
|
162
|
+
this.assertCanWrite()
|
|
67
163
|
this.logger.debug(`${this.uri} sends buffer ${buffer.length} bytes`)
|
|
68
164
|
const localSequenceNumber = this.localSequenceNumber++
|
|
69
165
|
const handledPromise = this.awaitProcessed()
|
|
@@ -78,44 +174,59 @@ export class WriterInstance implements Writer {
|
|
|
78
174
|
buffer: AsyncIterable<T>,
|
|
79
175
|
transform?: (x: T) => Uint8Array,
|
|
80
176
|
) {
|
|
177
|
+
this.assertCanWrite()
|
|
81
178
|
this.openStreams += 1
|
|
82
179
|
const t = transform || ((x: unknown) => <Uint8Array>x)
|
|
83
180
|
const stream = this.client.sendStreamMessage()
|
|
84
181
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
182
|
+
try {
|
|
183
|
+
// Message-level ack that signals the whole stream message is fully handled.
|
|
184
|
+
const handledPromise = this.awaitProcessed()
|
|
185
|
+
const writeStreamMessageChunk = promisify(stream.write.bind(stream))
|
|
186
|
+
const localSequenceNumber = this.localSequenceNumber++
|
|
187
|
+
await writeStreamMessageChunk({
|
|
188
|
+
id: {
|
|
189
|
+
channel: this.uri,
|
|
190
|
+
localSequenceNumber,
|
|
191
|
+
runner: this.runnerId,
|
|
192
|
+
},
|
|
193
|
+
})
|
|
95
194
|
|
|
96
|
-
|
|
195
|
+
// First response confirms stream id registration on the remote side.
|
|
196
|
+
const id = await this.raceWithCancellation(
|
|
197
|
+
new Promise((res) => stream.once('data', res)),
|
|
198
|
+
)
|
|
97
199
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
200
|
+
this.logger.debug(
|
|
201
|
+
`${this.uri} streams message with id ${JSON.stringify(id)}`,
|
|
202
|
+
)
|
|
101
203
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
204
|
+
for await (const msg of buffer) {
|
|
205
|
+
const processedPromise = new Promise((res) => stream.once('data', res))
|
|
206
|
+
await writeStreamMessageChunk({ data: { data: t(msg) } })
|
|
207
|
+
// Await a message on the stream, indicating that the chunk has been processed
|
|
208
|
+
await processedPromise
|
|
209
|
+
}
|
|
108
210
|
|
|
109
|
-
|
|
211
|
+
stream.end()
|
|
110
212
|
|
|
111
|
-
|
|
213
|
+
await handledPromise
|
|
214
|
+
} finally {
|
|
215
|
+
this.openStreams -= 1
|
|
112
216
|
|
|
113
|
-
|
|
217
|
+
if (!stream.writableEnded) {
|
|
218
|
+
stream.end()
|
|
219
|
+
}
|
|
114
220
|
|
|
115
|
-
|
|
221
|
+
// If a close call was deferred while streaming, complete it now.
|
|
222
|
+
if (this.shouldClose.length > 0) {
|
|
223
|
+
await this.close()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
116
226
|
}
|
|
117
227
|
|
|
118
228
|
async string(msg: string): Promise<void> {
|
|
229
|
+
this.assertCanWrite()
|
|
119
230
|
this.logger.debug(`${this.uri} sends string ${msg.length} characters`)
|
|
120
231
|
const localSequenceNumber = this.localSequenceNumber++
|
|
121
232
|
const handledPromise = this.awaitProcessed()
|
|
@@ -143,6 +254,33 @@ export class WriterInstance implements Writer {
|
|
|
143
254
|
* @param issued - If true, indicates the close request originated remotely
|
|
144
255
|
*/
|
|
145
256
|
async close(issued = false): Promise<void> {
|
|
257
|
+
if (issued) {
|
|
258
|
+
if (!this.closed) {
|
|
259
|
+
// Remote initiated close: mark writer canceled to fail future writes.
|
|
260
|
+
this._canceled = true
|
|
261
|
+
|
|
262
|
+
// Notify processors so they can stop producing upstream work as well.
|
|
263
|
+
this.emitCancel()
|
|
264
|
+
}
|
|
265
|
+
this.closed = true
|
|
266
|
+
|
|
267
|
+
const cancelError = this.cancellationError()
|
|
268
|
+
this.rejectPendingProcessed(cancelError)
|
|
269
|
+
this.cancelSignal.abort(cancelError)
|
|
270
|
+
|
|
271
|
+
// Unblock any local close() callers waiting for streams to settle.
|
|
272
|
+
let waiting = this.shouldClose.pop()
|
|
273
|
+
while (waiting) {
|
|
274
|
+
waiting()
|
|
275
|
+
waiting = this.shouldClose.pop()
|
|
276
|
+
}
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (this.closed) {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
146
284
|
// Case 1: Active streams still running → wait until they finish
|
|
147
285
|
if (this.openStreams !== 0) {
|
|
148
286
|
await new Promise<void>((resolve) => this.shouldClose.push(resolve))
|
|
@@ -151,11 +289,10 @@ export class WriterInstance implements Writer {
|
|
|
151
289
|
|
|
152
290
|
// Case 2: No active streams → perform actual close
|
|
153
291
|
this.logger.debug(`${this.uri} closes stream`)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
292
|
+
this.closed = true
|
|
293
|
+
await this.notifyOrchestrator({
|
|
294
|
+
close: { channel: this.uri },
|
|
295
|
+
})
|
|
159
296
|
|
|
160
297
|
let resolve = this.shouldClose.pop()
|
|
161
298
|
while (resolve) {
|
|
@@ -169,7 +306,10 @@ export class WriterInstance implements Writer {
|
|
|
169
306
|
*/
|
|
170
307
|
handled(): void {
|
|
171
308
|
if (this.awaitingProcessed.length > 0) {
|
|
172
|
-
this.awaitingProcessed.shift()
|
|
309
|
+
this.awaitingProcessed.shift()!.resolve()
|
|
310
|
+
} else if (this.closed || this._canceled) {
|
|
311
|
+
// A late ack can arrive after a close/cancel race; nothing to resolve anymore.
|
|
312
|
+
return
|
|
173
313
|
} else {
|
|
174
314
|
this.logger.error(
|
|
175
315
|
'Expected to be waiting for a message to be processed, but this is not the case ' +
|
package/jest.config.js
DELETED