@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
package/minimal.ttl ADDED
@@ -0,0 +1,99 @@
1
+ @prefix prov: <http://www.w3.org/ns/prov#>.
2
+ @prefix sds: <https://w3id.org/sds#>.
3
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
4
+ @prefix owl: <http://www.w3.org/2002/07/owl#>.
5
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
6
+ @prefix sh: <http://www.w3.org/ns/shacl#>.
7
+ @prefix rdfc: <https://w3id.org/rdf-connect#>.
8
+
9
+ ############################################################
10
+ # General statements #
11
+ ############################################################
12
+ # sds:Activity is a prov:Activity
13
+ sds:Activity rdfs:subClassOf prov:Activity.
14
+
15
+ # rdfc:Processor too
16
+ rdfc:Processor rdfs:subClassOf sds:Activity.
17
+
18
+ # sds:implementationOf is subClassOf
19
+ sds:implementationOf rdfs:subPropertyOf rdfs:subClassOf.
20
+
21
+ ############################################################
22
+ # Javascript statements #
23
+ ############################################################
24
+ # specialized for js too
25
+ rdfc:jsImplementationOf rdfs:subPropertyOf sds:implementationOf.
26
+
27
+ # A node runner, runs things that are rdfc:jsImplementationOf rdfc:Processor (aka, rdfs:subClassOf prov:Activity)
28
+ rdfc:NodeRunner a rdfc:Runner;
29
+ rdfc:handlesSubjectsOf rdfc:jsImplementationOf;
30
+ rdfc:command "npx js-runner".
31
+
32
+ # This shouldn't be necessary, should work with sh:targetSubjectsOf
33
+ # rdfc:processor_definition <JsProcessorShape>.
34
+ #
35
+ # Shape that a Js Processor should fulfil;
36
+ [ ] a sh:NodeShape;
37
+ # This shouldn't be necessary,should work with sh:targetSubjectsOf and this isn't a real Class
38
+ sh:targetClass <JsProcessorShape>;
39
+ # We target it with jsImplementationOf
40
+ sh:targetSubjectsOf rdfc:jsImplementationOf;
41
+ sh:property [
42
+ sh:path rdfc:entrypoint;
43
+ sh:name "location";
44
+ sh:minCount 1;
45
+ sh:maxCount 1;
46
+ sh:datatype xsd:iri;
47
+ ], [
48
+ sh:path rdfc:file;
49
+ sh:name "file";
50
+ sh:minCount 1;
51
+ sh:maxCount 1;
52
+ sh:datatype xsd:iri;
53
+ ], [
54
+ sh:path rdfc:class;
55
+ sh:name "clazz";
56
+ sh:maxCount 1;
57
+ sh:datatype xsd:string;
58
+ ].
59
+
60
+ ############################################################
61
+ # Processor statements #
62
+ ############################################################
63
+ rdfc:FooBarProcessor a owl:Class;
64
+ rdfs:label "My Epic FooBar Processor";
65
+ rdfs:description "FooBars everything!";
66
+ rdfc:jsImplementationOf rdfc:Processor;
67
+ rdfc:entrypoint <./>;
68
+ rdfc:file <./lib/processors.js>;
69
+ rdfc:class "FooBarProcessor".
70
+
71
+ [ ] a sh:NodeShape;
72
+ sh:targetClass rdfc:FooBarProcessor;
73
+ sh:property [
74
+ sh:path rdfc:reader;
75
+ sh:name "reader";
76
+ sh:minCount 1;
77
+ sh:maxCount 1;
78
+ sh:class rdfc:Reader;
79
+ ], [
80
+ sh:path rdfc:writer;
81
+ sh:name "writer";
82
+ sh:maxCount 1;
83
+ sh:class rdfc:Writer;
84
+ ].
85
+
86
+ ############################################################
87
+ # Pipeline statements #
88
+ ############################################################
89
+ <> a rdfc:Pipeline;
90
+ rdfc:consistsOf [
91
+ rdfc:instantiates rdfc:NodeRunner;
92
+ rdfc:processor <foobar>;
93
+ ].
94
+
95
+ <incomingMessages> a rdfc:Reader, rdfc:Writer.
96
+ <foobar> a rdfc:FooBarProcessor;
97
+ rdfc:reader <incomingMessages>;
98
+ rdfc:writer <incomingMessages>.
99
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdfc/js-runner",
3
- "version": "2.0.0-alpha.9",
3
+ "version": "3.0.0",
4
4
  "main": "lib/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -31,13 +31,14 @@
31
31
  "devDependencies": {
32
32
  "@eslint/js": "^9.21.0",
33
33
  "@rdfjs/types": "^2.0.1",
34
+ "@grpc/grpc-js": "^1.12.6",
34
35
  "@types/jest": "^29.5.14",
35
36
  "@types/jsonld": "^1.5.15",
36
37
  "@types/n3": "^1.21.1",
37
38
  "@types/node": "^22.13.5",
38
39
  "@typescript-eslint/eslint-plugin": "^8.25.0",
39
40
  "@typescript-eslint/parser": "^8.25.0",
40
- "@vitest/coverage-v8": "^3.0.7",
41
+ "@vitest/coverage-v8": "^3.2.4",
41
42
  "eslint": "^9.21.0",
42
43
  "eslint-config-prettier": "^10.0.1",
43
44
  "eslint-plugin-prettier": "^5.2.3",
@@ -50,11 +51,10 @@
50
51
  "tsc-alias": "^1.8.10",
51
52
  "typescript": "^5.7.3",
52
53
  "typescript-eslint": "^8.25.0",
53
- "vitest": "^3.0.7"
54
+ "vitest": "^3.2.4"
54
55
  },
55
56
  "dependencies": {
56
- "@grpc/grpc-js": "^1.12.6",
57
- "@rdfc/proto": "^0.1.2-alpha.1",
57
+ "@rdfc/proto": "^0.1.2",
58
58
  "@treecg/types": "^0.4.6",
59
59
  "jsonld": "^8.3.3",
60
60
  "jsonld-streaming-parser": "^5.0.0",
package/src/client.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import * as grpc from '@grpc/grpc-js'
2
2
  import { promisify } from 'util'
3
- import { RunnerClient, RunnerMessage } from '@rdfc/proto'
4
- import winston from 'winston'
3
+ import { RunnerClient, ToRunner } from '@rdfc/proto'
4
+ import { createLogger } from 'winston'
5
5
  import { RpcTransport } from './logger'
6
6
  import { Runner } from './runner'
7
7
 
8
8
  export async function start(addr: string, uri: string) {
9
9
  const client = new RunnerClient(addr, grpc.credentials.createInsecure())
10
10
 
11
- const logger = winston.createLogger({
11
+ const logger = createLogger({
12
12
  transports: [
13
13
  new RpcTransport({
14
14
  entities: [uri, 'cli'],
@@ -25,28 +25,25 @@ export async function start(addr: string, uri: string) {
25
25
 
26
26
  await writable({ identify: { uri } })
27
27
 
28
- let processorsEnd!: (v: unknown) => unknown
29
- const processorsEnded = new Promise((res) => (processorsEnd = res))
30
- ;(async () => {
28
+ /* eslint-disable no-async-promise-executor */
29
+ await new Promise(async (res) => {
31
30
  for await (const chunk of stream) {
32
- const msg: RunnerMessage = chunk
31
+ const msg: ToRunner = chunk
33
32
  if (msg.proc) {
34
33
  await runner.addProcessor(msg.proc)
35
34
  }
36
35
  if (msg.start) {
37
- runner.start().then(processorsEnd)
36
+ runner.start().then(res)
38
37
  }
39
38
 
40
39
  await runner.handleOrchMessage(msg)
41
40
  }
42
41
 
43
42
  logger.error('Stream ended')
44
- })()
45
-
46
- await processorsEnded
43
+ })
47
44
 
48
45
  logger.info('All processors are finished')
49
46
  stream.end()
50
47
  client.close()
51
- process.exit(0)
48
+ setTimeout(() => process.exit(0), 500)
52
49
  }
package/src/logger.ts CHANGED
@@ -1,4 +1,4 @@
1
- import winston, { Logger } from 'winston'
1
+ import { createLogger, LogEntry, Logger } from 'winston'
2
2
  import Transport from 'winston-transport'
3
3
 
4
4
  import * as grpc from '@grpc/grpc-js'
@@ -21,7 +21,7 @@ export class RpcTransport extends Transport {
21
21
  this.aliases = opts.aliases || []
22
22
  }
23
23
 
24
- log(info: winston.LogEntry, callback: () => void) {
24
+ log(info: LogEntry, callback: () => void) {
25
25
  if (!this.stream.closed) {
26
26
  this.stream.write(
27
27
  {
@@ -55,7 +55,7 @@ export function extendLogger(baseLogger: Logger, newEntity: string): Logger {
55
55
  return t
56
56
  })
57
57
 
58
- return winston.createLogger({
58
+ return createLogger({
59
59
  level: baseLogger.level,
60
60
  format: baseLogger.format,
61
61
  defaultMeta: baseLogger.defaultMeta,
package/src/reader.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import { ClientReadableStream } from '@grpc/grpc-js'
2
- import { DataChunk, Message, RunnerClient, StreamMessage } from '@rdfc/proto'
3
- import winston from 'winston'
2
+ import {
3
+ DataChunk,
4
+ ReceivingMessage,
5
+ ReceivingStreamMessage,
6
+ RunnerClient,
7
+ } from '@rdfc/proto'
8
+ import { Logger } from 'winston'
4
9
  import {
5
10
  AnyConvertor,
6
11
  Convertor,
@@ -8,6 +13,8 @@ import {
8
13
  StreamConvertor,
9
14
  StringConvertor,
10
15
  } from './convertor'
16
+ import { Writable } from './runner'
17
+ import { promisify } from 'util'
11
18
 
12
19
  export type Any =
13
20
  | {
@@ -28,41 +35,47 @@ export interface Reader {
28
35
  anys(): AsyncIterable<Any>
29
36
  }
30
37
 
38
+ type Todo<T> = {
39
+ item: T
40
+ onComplete: () => void
41
+ }
42
+
31
43
  class MyIter<T> implements AsyncIterable<T> {
32
44
  private convertor: Convertor<T>
33
- private queue: (T | undefined)[] = []
45
+ private queue: Todo<T | undefined>[] = []
34
46
  private resolveNext: ((value: undefined) => void) | null = null
35
47
 
36
48
  constructor(convertor: Convertor<T>) {
37
49
  this.convertor = convertor
38
50
  }
39
51
 
40
- push(buffer: Uint8Array) {
52
+ push(buffer: Uint8Array, onComplete: () => void) {
41
53
  const item = this.convertor.from(buffer)
42
- this.queue.push(item)
54
+ this.queue.push({ item, onComplete })
43
55
  if (this.resolveNext) {
44
56
  this.resolveNext(undefined)
45
57
  this.resolveNext = null
46
58
  }
47
59
  }
48
60
 
49
- close() {
50
- this.queue.push(undefined)
61
+ close(onComplete: () => void) {
62
+ this.queue.push({ item: undefined, onComplete })
51
63
  if (this.resolveNext) {
52
64
  this.resolveNext(undefined)
53
65
  this.resolveNext = null
54
66
  }
55
67
  }
56
68
 
57
- async pushStream(chunks: ClientReadableStream<DataChunk>) {
69
+ async pushStream(chunks: AsyncIterable<DataChunk>, onComplete: () => void) {
70
+ // This is an async generator that transforms DataChunks to Buffers
58
71
  const stream = (async function* (stream) {
59
- for await (const c of stream) {
60
- const chunk: DataChunk = c
72
+ for await (const chunk of stream) {
61
73
  yield chunk.data
62
74
  }
63
75
  })(chunks)
76
+
64
77
  const item = await this.convertor.fromStream(stream)
65
- this.queue.push(item)
78
+ this.queue.push({ item, onComplete })
66
79
  if (this.resolveNext) {
67
80
  this.resolveNext(undefined)
68
81
  this.resolveNext = null
@@ -72,9 +85,15 @@ class MyIter<T> implements AsyncIterable<T> {
72
85
  async *[Symbol.asyncIterator]() {
73
86
  while (true) {
74
87
  if (this.queue.length > 0) {
75
- const item = this.queue.shift()!
76
- if (item === undefined) break
88
+ const { item, onComplete } = this.queue.shift()!
89
+ if (item === undefined) {
90
+ onComplete()
91
+ break
92
+ }
77
93
  yield item
94
+ // Note: execution pauses at `yield` until the consumer calls `.next()` again.
95
+ // We call onComplete *after* resuming, so the producer knows the item was actually consumed.
96
+ onComplete()
78
97
  } else {
79
98
  await new Promise<undefined>((resolve) => (this.resolveNext = resolve))
80
99
  }
@@ -85,59 +104,217 @@ class MyIter<T> implements AsyncIterable<T> {
85
104
  export class ReaderInstance implements Reader {
86
105
  private client: RunnerClient
87
106
  readonly uri: string
88
- private logger: winston.Logger
107
+ private logger: Logger
108
+ private readonly notifyOrchestrator: Writable
89
109
 
90
- private iterators: MyIter<unknown>[] = []
110
+ private consumers: MyIter<unknown>[] = []
91
111
 
92
- constructor(uri: string, client: RunnerClient, logger: winston.Logger) {
112
+ constructor(
113
+ uri: string,
114
+ client: RunnerClient,
115
+ notifyOrchestrator: Writable,
116
+ logger: Logger,
117
+ ) {
93
118
  this.uri = uri
94
119
  this.client = client
95
120
  this.logger = logger
121
+ this.notifyOrchestrator = notifyOrchestrator
96
122
  }
97
123
 
98
124
  anys(): AsyncIterable<Any> {
99
125
  const iter = new MyIter(AnyConvertor)
100
- this.iterators.push(iter)
126
+ this.consumers.push(iter)
101
127
  return iter
102
128
  }
103
129
 
104
130
  strings(): AsyncIterable<string> {
105
131
  const iter = new MyIter(StringConvertor)
106
- this.iterators.push(iter)
132
+ this.consumers.push(iter)
107
133
  return iter
108
134
  }
109
135
 
110
136
  buffers(): AsyncIterable<Uint8Array> {
111
137
  const iter = new MyIter(NoConvertor)
112
- this.iterators.push(iter)
138
+ this.consumers.push(iter)
113
139
  return iter
114
140
  }
115
141
 
116
142
  streams(): AsyncIterable<AsyncGenerator<Uint8Array>> {
117
143
  const iter = new MyIter(StreamConvertor)
118
- this.iterators.push(iter)
144
+ this.consumers.push(iter)
119
145
  return iter
120
146
  }
121
147
 
122
- handleMsg(msg: Message) {
148
+ handleMsg(msg: ReceivingMessage) {
123
149
  this.logger.debug(`${this.uri} handling message`)
124
- console.log(`${this.uri} handling message`)
125
- for (const iter of this.iterators) {
126
- iter.push(msg.data)
150
+
151
+ const promises = []
152
+ for (const iter of this.consumers) {
153
+ promises.push(new Promise((res) => iter.push(msg.data, () => res(null))))
127
154
  }
155
+
156
+ Promise.all(promises).then(() =>
157
+ this.notifyOrchestrator({
158
+ processed: {
159
+ globalSequenceNumber: msg.globalSequenceNumber,
160
+ channel: this.uri,
161
+ },
162
+ }),
163
+ )
128
164
  }
129
165
 
130
166
  close() {
131
- for (const iter of this.iterators) {
132
- iter.close()
167
+ for (const iter of this.consumers) {
168
+ iter.close(() => {})
133
169
  }
134
170
  }
135
171
 
136
- handleStreamingMessage(msg: StreamMessage) {
172
+ // There is a stream message available for this reader
173
+ async handleStreamingMessage({
174
+ channel,
175
+ globalSequenceNumber,
176
+ }: ReceivingStreamMessage) {
137
177
  this.logger.debug(`${this.uri} handling streaming message`)
138
- const chunks = this.client.receiveStreamMessage(msg.id!)
139
- for (const iter of this.iterators) {
140
- iter.pushStream(chunks)
178
+
179
+ const chunks = this.client.receiveStreamMessage()
180
+ const writeControlMessage = promisify(chunks.write.bind(chunks))
181
+ const consumersConsumed = []
182
+
183
+ // After each chunk is handled by all consumer, emit a processed message
184
+ let idx = 0
185
+ const messageIterators = fanoutStream(
186
+ chunks,
187
+ this.consumers.length,
188
+ async () => {
189
+ await writeControlMessage({ streamSequenceNumber: idx++ })
190
+ },
191
+ )
192
+
193
+ for (const consumer of this.consumers) {
194
+ consumersConsumed.push(
195
+ new Promise((res) =>
196
+ consumer.pushStream(messageIterators.pop()!, () => res(null)),
197
+ ),
198
+ )
199
+ }
200
+
201
+ await writeControlMessage({ globalSequenceNumber })
202
+
203
+ Promise.all(consumersConsumed).then(() => {
204
+ console.log('Writing processed for streaming message')
205
+ this.notifyOrchestrator({ processed: { globalSequenceNumber, channel } })
206
+ })
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Helper function to tee a stream `numConsumers` times
212
+ * When each tee'd stream has handled a chunk, call {@link onAllHandled}
213
+ */
214
+ function fanoutStream<T>(
215
+ stream: ClientReadableStream<T>,
216
+ numConsumers: number,
217
+ onAllHandled: () => void | Promise<void>,
218
+ ): AsyncIterable<T>[] {
219
+ type Waiter = (value: IteratorResult<T>) => void
220
+
221
+ let ended = false
222
+ const buffer: T[] = []
223
+ const pending: Waiter[] = []
224
+ let activeConsumers = numConsumers
225
+
226
+ // consumer bookkeeping
227
+ let awaitingAck = 0
228
+
229
+ function pushChunk(chunk: T) {
230
+ buffer.push(chunk)
231
+ flush()
232
+ }
233
+
234
+ function flush() {
235
+ while (buffer.length > 0 && pending.length > 0) {
236
+ const chunk = buffer[0] // keep until all consumers ack
237
+ const waiter = pending.shift()!
238
+ waiter({ value: chunk, done: false })
239
+ awaitingAck++
240
+ }
241
+ }
242
+
243
+ function end() {
244
+ ended = true
245
+ while (pending.length > 0) {
246
+ const waiter = pending.shift()!
247
+ waiter({ value: undefined, done: true })
141
248
  }
142
249
  }
250
+
251
+ stream.on('data', (chunk: T) => {
252
+ pushChunk(chunk)
253
+ })
254
+
255
+ stream.on('end', () => {
256
+ end()
257
+ })
258
+
259
+ stream.on('error', (err) => {
260
+ while (pending.length > 0) {
261
+ const waiter = pending.shift()!
262
+ waiter({ value: undefined, done: true })
263
+ }
264
+ throw err
265
+ })
266
+
267
+ function makeIterable(): AsyncIterable<T> {
268
+ return {
269
+ [Symbol.asyncIterator]() {
270
+ return {
271
+ next(): Promise<IteratorResult<T>> {
272
+ if (buffer.length > 0) {
273
+ const chunk = buffer[0]
274
+ awaitingAck++
275
+ return Promise.resolve({ value: chunk, done: false })
276
+ }
277
+ if (ended) {
278
+ return Promise.resolve({ value: undefined, done: true })
279
+ }
280
+ return new Promise((resolve) => {
281
+ pending.push(resolve)
282
+ })
283
+ },
284
+ async return() {
285
+ activeConsumers--
286
+ if (activeConsumers === 0) {
287
+ end()
288
+ }
289
+ return { value: undefined, done: true }
290
+ },
291
+ }
292
+ },
293
+ }
294
+ }
295
+
296
+ async function ack() {
297
+ awaitingAck--
298
+ if (awaitingAck === 0) {
299
+ // all consumers done with the current chunk
300
+ buffer.shift() // drop it
301
+ await onAllHandled()
302
+ flush() // continue with next chunk
303
+ }
304
+ }
305
+
306
+ // wrap consumer so they *must* call ack() after processing
307
+ function wrap(iterable: AsyncIterable<T>): AsyncIterable<T> {
308
+ return {
309
+ async *[Symbol.asyncIterator]() {
310
+ for await (const item of iterable) {
311
+ yield item
312
+ await ack()
313
+ }
314
+ },
315
+ }
316
+ }
317
+
318
+ const rawIterables = Array.from({ length: numConsumers }, makeIterable)
319
+ return rawIterables.map(wrap)
143
320
  }