@t4h.framework/fs 0.13.1 → 0.14.2

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 (78) hide show
  1. package/.ai/skills/framework-fs/SKILL.md +87 -186
  2. package/.ai/skills/framework-fs-split/SKILL.md +134 -306
  3. package/.ai/skills/framework-fs-write/SKILL.md +54 -212
  4. package/CHANGELOG.md +37 -0
  5. package/README.md +29 -345
  6. package/dist/activities/FileJoinActivity.d.ts +1 -2
  7. package/dist/activities/FileJoinActivity.d.ts.map +1 -1
  8. package/dist/activities/FileJoinActivity.js +38 -43
  9. package/dist/activities/FileJoinActivity.js.map +1 -1
  10. package/dist/activities/FileReadActivity.d.ts.map +1 -1
  11. package/dist/activities/FileReadActivity.js +18 -15
  12. package/dist/activities/FileReadActivity.js.map +1 -1
  13. package/dist/activities/FileSplitActivity.d.ts +21 -34
  14. package/dist/activities/FileSplitActivity.d.ts.map +1 -1
  15. package/dist/activities/FileSplitActivity.js +23 -38
  16. package/dist/activities/FileSplitActivity.js.map +1 -1
  17. package/dist/constants/operations.d.ts.map +1 -1
  18. package/dist/constants/operations.js +3 -14
  19. package/dist/constants/operations.js.map +1 -1
  20. package/dist/fs.d.ts +5 -1
  21. package/dist/fs.d.ts.map +1 -1
  22. package/dist/helpers/CSV.d.ts +2 -2
  23. package/dist/helpers/CSV.d.ts.map +1 -1
  24. package/dist/helpers/CSV.js +20 -24
  25. package/dist/helpers/CSV.js.map +1 -1
  26. package/dist/helpers/group-by-key.d.ts +8 -0
  27. package/dist/helpers/group-by-key.d.ts.map +1 -0
  28. package/dist/helpers/group-by-key.js +35 -0
  29. package/dist/helpers/group-by-key.js.map +1 -0
  30. package/dist/helpers/group-records.d.ts +2 -2
  31. package/dist/helpers/group-records.d.ts.map +1 -1
  32. package/dist/helpers/group-records.js +3 -3
  33. package/dist/helpers/group-records.js.map +1 -1
  34. package/dist/helpers/in-chunks.d.ts +5 -0
  35. package/dist/helpers/in-chunks.d.ts.map +1 -0
  36. package/dist/helpers/in-chunks.js +17 -0
  37. package/dist/helpers/in-chunks.js.map +1 -0
  38. package/dist/helpers/split-records.d.ts +4 -3
  39. package/dist/helpers/split-records.d.ts.map +1 -1
  40. package/dist/helpers/split-records.js +60 -41
  41. package/dist/helpers/split-records.js.map +1 -1
  42. package/dist/helpers/where-matcher.d.ts +21 -0
  43. package/dist/helpers/where-matcher.d.ts.map +1 -0
  44. package/dist/helpers/where-matcher.js +11 -0
  45. package/dist/helpers/where-matcher.js.map +1 -0
  46. package/dist/models/BinaryRecordKey.d.ts +27 -0
  47. package/dist/models/BinaryRecordKey.d.ts.map +1 -0
  48. package/dist/models/BinaryRecordKey.js +52 -0
  49. package/dist/models/BinaryRecordKey.js.map +1 -0
  50. package/dist/models/BinarySource.d.ts +45 -0
  51. package/dist/models/BinarySource.d.ts.map +1 -0
  52. package/dist/models/BinarySource.js +58 -0
  53. package/dist/models/BinarySource.js.map +1 -0
  54. package/dist/models/BinarySplit.d.ts +45 -0
  55. package/dist/models/BinarySplit.d.ts.map +1 -0
  56. package/dist/models/BinarySplit.js +81 -0
  57. package/dist/models/BinarySplit.js.map +1 -0
  58. package/dist/models/BinarySplitInput.d.ts +21 -0
  59. package/dist/models/BinarySplitInput.d.ts.map +1 -0
  60. package/dist/models/BinarySplitInput.js +85 -0
  61. package/dist/models/BinarySplitInput.js.map +1 -0
  62. package/dist/models/Database.d.ts +15 -0
  63. package/dist/models/Database.d.ts.map +1 -0
  64. package/dist/models/Database.js +57 -0
  65. package/dist/models/Database.js.map +1 -0
  66. package/dist/models/OpenKeyGroup.d.ts +23 -0
  67. package/dist/models/OpenKeyGroup.d.ts.map +1 -0
  68. package/dist/models/OpenKeyGroup.js +42 -0
  69. package/dist/models/OpenKeyGroup.js.map +1 -0
  70. package/dist/models/SplitDatabaseWorker.d.ts +12 -0
  71. package/dist/models/SplitDatabaseWorker.d.ts.map +1 -0
  72. package/dist/models/SplitDatabaseWorker.js +65 -0
  73. package/dist/models/SplitDatabaseWorker.js.map +1 -0
  74. package/dist/workers/split-database.d.ts +10 -0
  75. package/dist/workers/split-database.d.ts.map +1 -0
  76. package/dist/workers/split-database.js +43 -0
  77. package/dist/workers/split-database.js.map +1 -0
  78. package/package.json +3 -3
package/README.md CHANGED
@@ -1,367 +1,51 @@
1
1
  # @t4h.framework/fs
2
2
 
3
- File system claim for reading and writing files within activities, plus the
4
- workflow-facing file activities: `split` fans a large file out into one child
5
- workflow per slice, `join` folds the batch results back into a single file,
6
- `read` brings a small byte window (a header, a trailer) into the workflow as a
7
- string, and `write` turns content the workflow produced into a file.
3
+ File functions for T4H Framework workflows. `read` brings a small byte window
4
+ into the workflow, `write` turns workflow content into a file, `split` starts
5
+ one child workflow per record of a file, `join` folds the batch results back
6
+ into one file, and `asset` reads a file that ships with the project. Only
7
+ references to the files go into the workflow history.
8
8
 
9
- ## Claim
9
+ ## Install
10
10
 
11
- ### FileSystemClaim
12
-
13
- Abstract dependency for file system operations. An activity declares this claim when it needs to read or write files. The runtime provides a concrete implementation (e.g. local disk, S3, GCS).
14
-
15
- ```typescript
16
- import { FileSystemClaim } from '@t4h.framework/fs'
17
- ```
18
-
19
- ### Interface
20
-
21
- ```typescript
22
- abstract class FileSystemClaim {
23
- public abstract write(
24
- path: string,
25
- readable: Readable | Buffer,
26
- contentType?: string,
27
- ): Binary | Promise<Binary>
28
- public abstract read(path: string): Binary | Promise<Binary>
29
-
30
- public abstract slice(
31
- binary: Binary,
32
- range: { start: number; end: number },
33
- encoding?: BufferEncoding,
34
- ): Binary | Promise<Binary>
35
- }
36
- ```
37
-
38
- ### Usage inside an activity
39
-
40
- ```typescript
41
- import { SyncActivity, Claim } from '@t4h.framework/core'
42
- import { FileSystemClaim } from '@t4h.framework/fs'
43
-
44
- class SaveReportActivity extends SyncActivity<
45
- [data: Buffer],
46
- { path: string },
47
- any
48
- > {
49
- private readonly claims = new Claim({
50
- fs: FileSystemClaim,
51
- })
52
-
53
- public async toInput(data: Buffer) {
54
- return { path: 'reports/output.pdf' }
55
- }
56
-
57
- public async run(input: { path: string }) {
58
- const binary = await this.claims.fs.write(input.path, Buffer.from('...'))
59
- return { contentType: binary.contentType }
60
- }
61
-
62
- public toOutput(output: any) {
63
- return output
64
- }
65
- }
66
- ```
67
-
68
- ### Runtime provider
69
-
70
- The runtime must supply a concrete implementation:
71
-
72
- ```typescript
73
- { provide: FileSystemClaim, value: new LocalFileSystemClaimImpl() }
74
- ```
75
-
76
- ## File read
77
-
78
- ### read
79
-
80
- Reads a byte window of a `Binary` into the workflow as a decoded string. The
81
- output is recorded in the workflow history (replay never re-reads the file),
82
- which is why reads are capped at 1 MiB: this is the primitive for headers,
83
- trailers and single records — a data file flows through `split`, never through
84
- `read`.
85
-
86
- ```typescript
87
- import { read } from '@t4h.framework/fs'
88
-
89
- const header = await read(file, { range: { start: 0, end: 240 } })
90
-
91
- const trailer = await read(file, {
92
- range: { start: file.contentLength - 240, end: file.contentLength },
93
- })
11
+ ```bash
12
+ npm install @t4h.framework/fs @t4h.framework/core
94
13
  ```
95
14
 
96
- Options: `range` (default: the whole binary, subject to the same cap) and
97
- `encoding` (default `'utf8'`).
15
+ Requires Node.js 22.13 or later. Peer dependency: `@t4h.framework/core`.
98
16
 
99
- The activity tries `FileSystemClaim.slice` first — on a file-backed binary a
100
- trailer read is a ranged download, whatever the file size. Binaries the claim
101
- cannot slice (e.g. an SFTP reference) fall back to streaming: the stream is
102
- consumed up to the window and destroyed as soon as it is filled, so a header
103
- read pulls the first chunks only, whatever the file size. The one expensive
104
- case is a trailing window over a non-sliceable binary — the stream must
105
- traverse the whole file to reach it.
106
-
107
- Reading a slice is relative to the slice: `read(slice, { range: { start: 0,
108
- end: 10 } })` returns the first 10 bytes of the slice, not of the underlying
109
- file — slice ranges compose.
110
-
111
- ## File write
112
-
113
- ### write
114
-
115
- Stores content produced inside the workflow and returns its `Binary` — the only
116
- supported way to obtain a `Binary` from a string or a `Buffer`:
17
+ ## Usage
117
18
 
118
19
  ```typescript
119
- import { write } from '@t4h.framework/fs'
120
-
121
- const file = await write(JSON.stringify(state, null, 2), {
122
- contentType: 'application/json',
123
- })
20
+ import Type from 'typebox'
124
21
 
125
- await sftp.put('/out/state.json', file)
126
- ```
127
-
128
- Options: `contentType` (default: inferred from the filename, then
129
- `application/octet-stream`), `filename` (default: the activity id) and
130
- `encoding` for a string content (default `'utf8'`; a `Buffer` always travels as
131
- base64).
132
-
133
- **Never implement `Binary` in an App.** Passing a value to a primitive makes it
134
- the input of a durable activity, and that input is serialized to storage before
135
- the activity runs — a `Binary` holding bytes in memory has nothing to serialize
136
- into, and the activity may run in another process after a replay. A hand-rolled
137
- class can pass locally (the dev server keeps values in memory) and fail once
138
- deployed.
139
-
140
- `write` takes content already in workflow memory — a state file, a report, a
141
- return file. To build a large file out of many records, use `split` + `join`,
142
- which stream through storage.
143
-
144
- ## File split
145
-
146
- ### split
147
-
148
- Splits a file into slices, starts one child workflow per slice as
149
- a single batch, and suspends the parent until every child is terminal —
150
- regardless of each child succeeding or failing (a rejected child never
151
- rejects the batch):
152
-
153
- ```typescript
154
- import { split, Split } from '@t4h.framework/fs'
22
+ import { type Binary, Workflow } from '@t4h.framework/core'
23
+ import { join, split, Split } from '@t4h.framework/fs'
155
24
 
156
25
  const processRecord = new Workflow(
157
26
  { id: 'process-record', schema: Type.String() },
158
- async (record: string) => {
159
- // record is the raw slice content
160
- },
27
+ async (record: string) => record.slice(0, 10),
161
28
  )
162
29
 
163
- const main = new Workflow({ id: 'main', schema: fileSchema }, async (file: Binary) => {
164
- const batch = await split(file, processRecord, {
165
- delimiter: Split.line(),
166
- })
30
+ const main = new Workflow(
31
+ { id: 'main', schema: Type.Any() },
32
+ async (file: Binary) => {
33
+ const batch = await split(file, processRecord, {
34
+ delimiter: Split.line(),
35
+ timeoutInMs: 3_600_000,
36
+ })
167
37
 
168
- // this line only runs once the whole batch is terminal;
169
- // batch: WorkflowBatchRef — { id: string, count: number }
170
- })
171
- ```
172
-
173
- The source is read once, as a stream:
174
-
175
- 1. The source is read once, worker side: cut into records, filtered by
176
- `where` when given, grouped by `every`, and each slice goes into the batch
177
- as a string as soon as it is complete. Nothing collects the slices, and a
178
- slice may take up to 3 MiB: past that the split fails
179
- while it reads, at the byte the slice starts.
180
- 2. The runtime reads that stream once and stores the batch as it goes; no copy
181
- of the file is written and no slice is read back by range.
182
- 3. The activity output is just the batch reference (`{ id, count }`), so the
183
- parent history stays O(1) whatever the slice count. Per-child results are
184
- never aggregated: they live with each child run, to be inspected
185
- individually through the batch id.
186
-
187
- Child workflows declare their real input type (e.g. `Type.String()`); how the
188
- bytes reach them is a runtime concern.
189
-
190
- ### Delimiters
191
-
192
- The cut is defined by the dev through a serializable delimiter spec. When the
193
- delimiter is found at `[p, p + length)`, the previous slice ends at
194
- `p + keep.previous` and the next slice starts at `p + length - keep.next`; the
195
- bytes in between are discarded. The defaults discard the whole delimiter
196
- (classic line semantics).
197
-
198
- ```typescript
199
- Split.line() // cut at every '\n', delimiter discarded
200
- Split.char(';') // cut at any byte sequence
201
- Split.char(';', { previous: 1 }) // keep the ';' with the previous slice
202
- Split.char('\n10', { next: 2 }) // positional files, see below
203
- ```
204
-
205
- The last form targets positional files where a logical record spans multiple
206
- physical lines and a new record only starts when a line begins with a marker
207
- (the record type code):
208
-
209
- ```
210
- 10JOHN;123\n20detail-a\n20detail-b\n10MARY;456\n20detail-c
211
- └──────────── record 1 ───────────┘└─────── record 2 ──────┘
212
- ```
213
-
214
- With `Split.char('\n10', { next: 2 })` the newline between records is
215
- discarded, and the `10` marker — which is data, not separator — stays at the
216
- start of the next slice.
217
-
218
- ### Options
219
-
220
- ```typescript
221
- await split(file, workflow, {
222
- delimiter: Split.line(), // required: where to cut
223
- every: 10_000, // cut only every N occurrences (groups records)
224
- trim: { start: 241, end: 241 }, // bytes cut off each end before scanning
225
- skipEmpty: false, // keep zero-length slices (default: skipped)
226
- encoding: 'latin1', // delimiter bytes + slice decoding (default: utf8)
227
- timeoutInMs: 3_600_000, // max ms to await the batch; rejects on expiry
228
- })
229
- ```
230
-
231
- - **`every`** groups N records per child. With one million lines, `every:
232
- 10_000` starts 100 children instead of one million; each child receives a
233
- raw block with its inner delimiters untouched and iterates locally. `keep`
234
- arithmetic only applies at actual cut points.
235
- - **`trim`** cuts a fixed number of bytes off each end of the file before
236
- scanning — a positional header/trailer, which may not be aligned with the
237
- record delimiter (a record-start delimiter like `Split.char('\n10')` fuses
238
- the trailer into the last record's slice, so counting records would drop a
239
- valid one along with it). Trimmed content never becomes a slice nor counts
240
- toward `every`; fetch it with `read`. Getting the byte counts right is the
241
- caller's business — positional layouts make them constants:
242
-
243
- ```typescript
244
- const header = await read(file, { range: { start: 0, end: 240 } })
245
-
246
- const batch = await split(file, processInvoice, {
247
- delimiter: Split.char('\n10', { next: 2 }),
248
- trim: { start: 241, end: 241 }, // header + '\n', trailer + '\n'
249
- })
250
- ```
251
-
252
- - **`skipEmpty`** (default `true`) drops zero-length slices — consecutive
253
- delimiters, or a file starting/ending with one — so they never become child
254
- workflows. Turn it off when the slice index must align 1:1 with the record
255
- position in the source file. "Empty" means zero bytes: a whitespace-only
256
- line is a regular slice.
257
- - **`encoding`** converts the delimiter to bytes and decodes each slice into
258
- the string its child receives. Offsets are always computed over raw bytes,
259
- so multi-byte encodings never skew the cuts.
260
- - **`timeoutInMs`** is the maximum time in milliseconds to await the batch. On
261
- expiry the activity rejects (reason `timeout`) instead of suspending
262
- forever — the safety net for a child that never reaches a terminal state.
263
-
264
- ## File join
265
-
266
- The inverse of `split`: folds the results of a completed batch into a single
267
- file. Outputs stream in one at a time through `WorkflowBatchClaim` and flow
268
- straight into `FileSystemClaim.write` with backpressure, so memory stays
269
- bounded whatever the batch size — only the resulting binary reaches the
270
- workflow history.
271
-
272
- ```typescript
273
- import { join, split, Split } from '@t4h.framework/fs'
274
-
275
- const batch = await split(file, processRecord, { delimiter: Split.line() })
276
-
277
- const csv = await join(batch, {
278
- format: 'csv',
279
- columns: [
280
- { key: 'id', header: 'Código' },
281
- { key: 'value', header: 'Valor (R$)' },
282
- 'date', // shorthand: key === header
283
- ],
284
- })
285
- // csv: Binary
286
- ```
287
-
288
- ### Formats
289
-
290
- - **`lines`** — concatenates raw child outputs; the child owns the line
291
- format. Options: `header`, `separator` (default `'\n'`).
292
- - **`entries`** — NDJSON dump of `{ index, status, output }`, one entry per
293
- line. The format-agnostic view of the batch, rejected children included by
294
- default.
295
- - **`csv`** — projects object outputs into RFC 4180 escaped columns. Options:
296
- `columns` (required; each entry is `{ key, header? }` where `key` selects
297
- the value on the child output — a dot-path like `'meta.total'` reaches
298
- nested fields, matching the `where` semantics; a literal top-level key that
299
- contains dots wins over the nested path — and `header` is the label on the
300
- header row — a plain string is shorthand for `{ key }`), `delimiter`
301
- (default `','`), `header` (default `true`; emits the column labels as the
302
- first row).
303
-
304
- ### Status filter
305
-
306
- `status` selects which child results make it into the file, instead of an
307
- error policy: it defaults to `['fulfilled']` for `lines` and `csv`, and to
308
- both statuses for `entries`. Filtering is silent by design — a file of
309
- rejected children for reprocessing is one call away:
310
-
311
- ```typescript
312
- const failures = await join(batch, { format: 'entries', status: ['rejected'] })
313
- ```
314
-
315
- ### Where filter
316
-
317
- `where` narrows results with a [sift](https://github.com/crcn/sift.js)
318
- (MongoDB-style) query over each entry's output, applied after the `status`
319
- filter. The accepted subset is deliberate — only declarative, JSON-only
320
- operators: `$eq`, `$ne`, `$lt`, `$lte`, `$gt`, `$gte`, `$in`, `$nin`,
321
- `$exists`, `$not`, `$and`, `$or`, `$nor`, plus plain equality and dot-paths
322
- for nested fields:
323
-
324
- ```typescript
325
- const relevant = await join(batch, {
326
- format: 'csv',
327
- columns: ['id', 'total'],
328
- where: {
329
- valid: true, // plain strict equality
330
- total: { $gt: 100 }, // operators
331
- 'meta.situacao': 'aprovada', // dot-path into nested output
332
- $or: [{ ref: 'a' }, { ref: 'b' }],
38
+ return await join(batch, { format: 'lines', header: 'id' })
333
39
  },
334
- })
40
+ )
335
41
  ```
336
42
 
337
- `join` also accepts the child output type as a generic — top-level keys then
338
- narrow to its fields, with values typed by the field (dot-paths are accepted
339
- without narrowing):
340
-
341
- ```typescript
342
- type InvoiceResult = { id: string; valid: boolean; total: number }
343
-
344
- await join<InvoiceResult>(batch, {
345
- format: 'csv',
346
- columns: ['id'],
347
- where: { valid: true, total: { $gt: 100 } }, // { valid: 'yes' } rejects
348
- })
349
- ```
43
+ ## Documentation
350
44
 
351
- Anything that cannot serialize losslessly into the workflow history is
352
- rejected eagerly at `toInput` with a `TypeError`: functions (including
353
- `$where`), `RegExp` values (including `$regex`), `undefined`, and operators
354
- outside the subset — a dropped closure would make the replay filter
355
- differently than the first execution. For arbitrary predicates, write a
356
- custom activity in project code (the function then lives in versioned code,
357
- not in history).
45
+ - [Guide](https://tech4humans-brasil.github.io/framework/fs/)
46
+ - [API reference](https://tech4humans-brasil.github.io/framework/api/fs)
47
+ - [Changelog](./CHANGELOG.md)
358
48
 
359
- Two MongoDB semantics worth knowing: array fields match by containment
360
- (`{ tags: 'a' }` matches `tags: ['a', 'b']`), and equality against an object
361
- value compares deeply (never by reference). Comparisons never coerce types.
49
+ ## License
362
50
 
363
- Filtering runs inside the existing streaming loop, over outputs that are
364
- already hydrated one at a time — the query compiles once and adds one
365
- predicate call per entry, no extra serialization and no change to the memory
366
- profile. It is applied app-side: every child output is still fetched and
367
- parsed; `where` decides what enters the file, not what leaves the database.
51
+ MIT
@@ -32,8 +32,7 @@ export declare class FileJoinActivity extends SyncActivity<readonly [batch: Work
32
32
  run({ batch, options, filter, }: FileJoinActivityInput): Promise<Binary>;
33
33
  toOutput(output: Binary): Binary;
34
34
  private toStatus;
35
- private writeHeader;
36
- private toLine;
35
+ private toLines;
37
36
  }
38
37
  export declare function join<TOutput = unknown>(batch: WorkflowBatchCollectionRef, options: FileJoinOptions<TOutput>): Promise<Binary>;
39
38
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"FileJoinActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,MAAM,EAGX,YAAY,EAEZ,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EAGxB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAEvC,MAAM,MAAM,cAAc,GAAG,mBAAmB,CAAA;AAEhD,MAAM,MAAM,aAAa,CAAC,OAAO,GAAG,OAAO,IAAI,kBAAkB,CAAC,OAAO,CAAC,CAAA;AAE1E,KAAK,mBAAmB,CAAC,OAAO,IAAI;IAClC,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC/B,CAAA;AAED,KAAK,oBAAoB,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG;IAClE,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,KAAK,sBAAsB,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG;IACpE,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,KAAK,kBAAkB,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG;IAChE,MAAM,EAAE,KAAK,CAAA;IACb,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,CAAC,OAAO,GAAG,OAAO,IACzC,oBAAoB,CAAC,OAAO,CAAC,GAC7B,sBAAsB,CAAC,OAAO,CAAC,GAC/B,kBAAkB,CAAC,OAAO,CAAC,CAAA;AAE/B,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,eAAe,CAAA;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAA;CAC7B,CAAA;AAQD,qBAAa,gBAAiB,SAAQ,YAAY,CAChD,SAAS,CAAC,KAAK,EAAE,0BAA0B,EAAE,OAAO,EAAE,eAAe,CAAC,EACtE,qBAAqB,EACrB,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGrB;IAEK,OAAO,CACZ,KAAK,EAAE,0BAA0B,EACjC,OAAO,EAAE,eAAe,GACvB,qBAAqB;IAkBX,GAAG,CAAC,EACf,KAAK,EACL,OAAO,EACP,MAAM,GACP,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IA6BnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAIvC,OAAO,CAAC,QAAQ;YAaF,WAAW;IAazB,OAAO,CAAC,MAAM;CAef;AAED,wBAAsB,IAAI,CAAC,OAAO,GAAG,OAAO,EAC1C,KAAK,EAAE,0BAA0B,EACjC,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,GAChC,OAAO,CAAC,MAAM,CAAC,CAMjB"}
1
+ {"version":3,"file":"FileJoinActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EAGX,YAAY,EAEZ,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EAGxB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAEvC,MAAM,MAAM,cAAc,GAAG,mBAAmB,CAAA;AAEhD,MAAM,MAAM,aAAa,CAAC,OAAO,GAAG,OAAO,IAAI,kBAAkB,CAAC,OAAO,CAAC,CAAA;AAE1E,KAAK,mBAAmB,CAAC,OAAO,IAAI;IAClC,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC/B,CAAA;AAED,KAAK,oBAAoB,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG;IAClE,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,KAAK,sBAAsB,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG;IACpE,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,KAAK,kBAAkB,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG;IAChE,MAAM,EAAE,KAAK,CAAA;IACb,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,CAAC,OAAO,GAAG,OAAO,IACzC,oBAAoB,CAAC,OAAO,CAAC,GAC7B,sBAAsB,CAAC,OAAO,CAAC,GAC/B,kBAAkB,CAAC,OAAO,CAAC,CAAA;AAE/B,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,eAAe,CAAA;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAA;CAC7B,CAAA;AA2CD,qBAAa,gBAAiB,SAAQ,YAAY,CAChD,SAAS,CAAC,KAAK,EAAE,0BAA0B,EAAE,OAAO,EAAE,eAAe,CAAC,EACtE,qBAAqB,EACrB,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGrB;IAEK,OAAO,CACZ,KAAK,EAAE,0BAA0B,EACjC,OAAO,EAAE,eAAe,GACvB,qBAAqB;IAeX,GAAG,CAAC,EACf,KAAK,EACL,OAAO,EACP,MAAM,GACP,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAkBnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAIvC,OAAO,CAAC,QAAQ;YAWD,OAAO;CAWvB;AAED,wBAAsB,IAAI,CAAC,OAAO,GAAG,OAAO,EAC1C,KAAK,EAAE,0BAA0B,EACjC,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,GAChC,OAAO,CAAC,MAAM,CAAC,CAEjB"}
@@ -1,13 +1,30 @@
1
- import { once } from 'node:events';
2
- import { PassThrough } from 'node:stream';
1
+ import { getDefaultHighWaterMark, Readable } from 'node:stream';
3
2
  import { Claim, History, SyncActivity, WorkflowBatchClaim, WorkflowBatchQuery, } from '@t4h.framework/core';
4
3
  import { FileSystemClaim } from '../FileSystemClaim.js';
5
4
  import { CSV } from '../helpers/CSV.js';
6
- const extensions = {
7
- lines: 'txt',
8
- entries: 'ndjson',
9
- csv: 'csv',
5
+ const formats = {
6
+ lines: {
7
+ extension: 'txt',
8
+ status: ['fulfilled'],
9
+ header: ({ header, separator = '\n' }) => header && header + separator,
10
+ line: ({ output }, { separator = '\n' }) => (typeof output === 'string' ? output : JSON.stringify(output)) +
11
+ separator,
12
+ },
13
+ entries: {
14
+ extension: 'ndjson',
15
+ status: ['fulfilled', 'rejected'],
16
+ line: entry => JSON.stringify(entry) + '\n',
17
+ },
18
+ csv: {
19
+ extension: 'csv',
20
+ status: ['fulfilled'],
21
+ header: ({ header, columns, delimiter }) => header === false ? undefined : CSV.toHeader(columns, { delimiter }),
22
+ line: ({ output }, { columns, delimiter }) => CSV.toLine(output, columns, { delimiter }),
23
+ },
10
24
  };
25
+ function formatOf(options) {
26
+ return formats[options.format];
27
+ }
11
28
  export class FileJoinActivity extends SyncActivity {
12
29
  claims = new Claim({
13
30
  fs: FileSystemClaim,
@@ -16,30 +33,20 @@ export class FileJoinActivity extends SyncActivity {
16
33
  toInput(batch, options) {
17
34
  if (options.where)
18
35
  WorkflowBatchQuery.assertSerializable(options.where, 'where', 'Join where');
19
- const filter = {
20
- status: batch.status,
21
- where: batch.where,
22
- };
23
- if (!filter.status && !filter.where)
36
+ const { status, where } = batch;
37
+ if (!status && !where)
24
38
  return { batch: batch.id, options };
25
- return { batch: batch.id, options, filter };
39
+ return { batch: batch.id, options, filter: { status, where } };
26
40
  }
27
41
  async run({ batch, options, filter, }) {
28
- const stream = new PassThrough();
29
- const writing = this.claims.fs.write(`join/${this.id}.${extensions[options.format]}`, stream);
30
- const write = async (line) => {
31
- if (!stream.write(line))
32
- await once(stream, 'drain');
33
- };
34
42
  const entries = WorkflowBatchQuery.filter(this.claims.batch.outputs(batch), WorkflowBatchQuery.merge(filter, {
35
43
  status: this.toStatus(options, filter),
36
44
  where: options.where,
37
45
  }));
38
- await this.writeHeader(options, write);
39
- for await (const entry of entries)
40
- await write(this.toLine(entry, options));
41
- stream.end();
42
- return await writing;
46
+ return await this.claims.fs.write(`join/${this.id}.${formatOf(options).extension}`, Readable.from(this.toLines(entries, options), {
47
+ objectMode: false,
48
+ highWaterMark: getDefaultHighWaterMark(false),
49
+ }));
43
50
  }
44
51
  toOutput(output) {
45
52
  return output;
@@ -49,27 +56,15 @@ export class FileJoinActivity extends SyncActivity {
49
56
  return options.status;
50
57
  if (filter?.status)
51
58
  return;
52
- if (options.format === 'entries')
53
- return ['fulfilled', 'rejected'];
54
- return ['fulfilled'];
59
+ return formatOf(options).status;
55
60
  }
56
- async writeHeader(options, write) {
57
- if (options.format === 'lines' && options.header)
58
- await write(options.header + (options.separator ?? '\n'));
59
- if (options.format === 'csv' && options.header !== false)
60
- await write(CSV.toHeader(options.columns, { delimiter: options.delimiter }));
61
- }
62
- toLine(entry, options) {
63
- if (options.format === 'entries')
64
- return JSON.stringify(entry) + '\n';
65
- if (options.format === 'csv')
66
- return CSV.toLine(entry.output, options.columns, {
67
- delimiter: options.delimiter,
68
- });
69
- const value = typeof entry.output === 'string'
70
- ? entry.output
71
- : JSON.stringify(entry.output);
72
- return value + (options.separator ?? '\n');
61
+ async *toLines(entries, options) {
62
+ const format = formatOf(options);
63
+ const header = format.header?.(options);
64
+ if (header)
65
+ yield header;
66
+ for await (const entry of entries)
67
+ yield format.line(entry, options);
73
68
  }
74
69
  }
75
70
  export async function join(batch, options) {
@@ -1 +1 @@
1
- {"version":3,"file":"FileJoinActivity.js","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FileJoinActivity.js","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE/D,OAAO,EAEL,KAAK,EACL,OAAO,EACP,YAAY,EACZ,kBAAkB,EAIlB,kBAAkB,GAGnB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAoDvC,MAAM,OAAO,GAAgB;IAC3B,KAAK,EAAE;QACL,SAAS,EAAE,KAAK;QAChB,MAAM,EAAE,CAAC,WAAW,CAAC;QACrB,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,MAAM,IAAI,MAAM,GAAG,SAAS;QACtE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,GAAG,IAAI,EAAE,EAAE,EAAE,CACzC,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAC9D,SAAS;KACZ;IACD,OAAO,EAAE;QACP,SAAS,EAAE,QAAQ;QACnB,MAAM,EAAE,CAAC,WAAW,EAAE,UAAU,CAAC;QACjC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI;KAC5C;IACD,GAAG,EAAE;QACH,SAAS,EAAE,KAAK;QAChB,MAAM,EAAE,CAAC,WAAW,CAAC;QACrB,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CACzC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,CAAC;QACrE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CAC3C,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,CAAC;KAC7C;CACF,CAAA;AAED,SAAS,QAAQ,CAAC,OAAwB;IACxC,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAgC,CAAA;AAC/D,CAAC;AAED,MAAM,OAAO,gBAAiB,SAAQ,YAKrC;IACkB,MAAM,GAAG,IAAI,KAAK,CAAC;QAClC,EAAE,EAAE,eAAe;QACnB,KAAK,EAAE,kBAAkB;KAC1B,CAAC,CAAA;IAEK,OAAO,CACZ,KAAiC,EACjC,OAAwB;QAExB,IAAI,OAAO,CAAC,KAAK;YACf,kBAAkB,CAAC,kBAAkB,CACnC,OAAO,CAAC,KAAK,EACb,OAAO,EACP,YAAY,CACb,CAAA;QAEH,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;QAE/B,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,CAAA;QAE1D,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAA;IAChE,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,EACf,KAAK,EACL,OAAO,EACP,MAAM,GACgB;QACtB,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CACvC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAChC,kBAAkB,CAAC,KAAK,CAAC,MAAM,EAAE;YAC/B,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;YACtC,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB,CAAC,CACH,CAAA;QAED,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAC/B,QAAQ,IAAI,CAAC,EAAE,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,EAChD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE;YAC5C,UAAU,EAAE,KAAK;YACjB,aAAa,EAAE,uBAAuB,CAAC,KAAK,CAAC;SAC9C,CAAC,CACH,CAAA;IACH,CAAC;IAEM,QAAQ,CAAC,MAAc;QAC5B,OAAO,MAAM,CAAA;IACf,CAAC;IAEO,QAAQ,CACd,OAAwB,EACxB,MAA4B;QAE5B,IAAI,OAAO,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,MAAM,CAAA;QAEzC,IAAI,MAAM,EAAE,MAAM;YAAE,OAAM;QAE1B,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,CAAA;IACjC,CAAC;IAEO,KAAK,CAAC,CAAC,OAAO,CACpB,OAA2C,EAC3C,OAAwB;QAExB,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAA;QAChC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,CAAA;QAEvC,IAAI,MAAM;YAAE,MAAM,MAAM,CAAA;QAExB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO;YAAE,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IACtE,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,KAAiC,EACjC,OAAiC;IAEjC,OAAO,MAAM,OAAO,CAAC,UAAU,CAAC,gBAAgB,EAAE,KAAK,EAAE,OAAO,CAAC,CAAA;AACnE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"FileReadActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileReadActivity.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAI/E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,eAAe,CAAA;CACzB,CAAA;AAID,qBAAa,gBAAiB,SAAQ,YAAY,CAChD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,CAAC,EACnD,qBAAqB,EACrB,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAErB;IAEK,OAAO,CACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,eAAe,GACvB,qBAAqB;IAIX,GAAG,CAAC,EACf,MAAM,EACN,OAAO,GACR,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAyBnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;YAIzB,KAAK;YAYL,MAAM;YAoBN,OAAO;CA8BtB;AAED,wBAAsB,IAAI,CACxB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAEjB"}
1
+ {"version":3,"file":"FileReadActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileReadActivity.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAI/E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,eAAe,CAAA;CACzB,CAAA;AA0BD,qBAAa,gBAAiB,SAAQ,YAAY,CAChD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,CAAC,EACnD,qBAAqB,EACrB,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAErB;IAEK,OAAO,CACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,eAAe,GACvB,qBAAqB;IAIX,GAAG,CAAC,EACf,MAAM,EACN,OAAO,GACR,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAcnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;YAIzB,KAAK;YAYL,MAAM;YAeN,OAAO;CA2BtB;AAED,wBAAsB,IAAI,CACxB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAEjB"}
@@ -2,6 +2,18 @@ import { buffer } from 'node:stream/consumers';
2
2
  import { Claim, History, SyncActivity } from '@t4h.framework/core';
3
3
  import { FileSystemClaim } from '../FileSystemClaim.js';
4
4
  const FILE_READ_MAX_BYTES = 1024 * 1024;
5
+ function assertRange(range, contentLength) {
6
+ const length = range.end - range.start;
7
+ if (!Number.isInteger(range.start) ||
8
+ !Number.isInteger(range.end) ||
9
+ range.start < 0 ||
10
+ length < 0)
11
+ throw new TypeError('Read range must be a non-negative byte window');
12
+ if (range.end > contentLength)
13
+ throw new TypeError('Read range must not exceed the binary length');
14
+ if (length > FILE_READ_MAX_BYTES)
15
+ throw new RangeError(`Length ${String(length)} exceeded max ${String(FILE_READ_MAX_BYTES)}`);
16
+ }
5
17
  export class FileReadActivity extends SyncActivity {
6
18
  claims = new Claim({
7
19
  fs: FileSystemClaim,
@@ -12,13 +24,7 @@ export class FileReadActivity extends SyncActivity {
12
24
  async run({ binary, options, }) {
13
25
  const encoding = options.encoding ?? 'utf8';
14
26
  const range = options.range ?? { start: 0, end: binary.contentLength };
15
- const length = range.end - range.start;
16
- if (!Number.isInteger(range.start) || range.start < 0 || length < 0)
17
- throw new TypeError('Read range must be a non-negative byte window');
18
- if (range.end > binary.contentLength)
19
- throw new TypeError('Read range must not exceed the binary length');
20
- if (length > FILE_READ_MAX_BYTES)
21
- throw new RangeError(`Length ${String(length)} exceeded max ${String(FILE_READ_MAX_BYTES)}`);
27
+ assertRange(range, binary.contentLength);
22
28
  const slice = await this.slice(binary, range, encoding);
23
29
  if (slice)
24
30
  return (await buffer(await slice.stream())).toString(encoding);
@@ -36,21 +42,19 @@ export class FileReadActivity extends SyncActivity {
36
42
  }
37
43
  }
38
44
  async window(readable, range) {
39
- let collected;
40
45
  try {
41
- collected = await this.collect(readable, range);
46
+ const content = Buffer.concat(await this.collect(readable, range));
47
+ if (content.length < range.end - range.start)
48
+ throw new RangeError('Binary stream ended before the read window was filled');
49
+ return content;
42
50
  }
43
51
  finally {
44
52
  readable.destroy();
45
53
  }
46
- if (collected.size < range.end - range.start)
47
- throw new RangeError('Binary stream ended before the read window was filled');
48
- return Buffer.concat(collected.chunks);
49
54
  }
50
55
  async collect(readable, range) {
51
56
  const chunks = [];
52
57
  let position = 0;
53
- let size = 0;
54
58
  for await (const data of readable) {
55
59
  const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data);
56
60
  const chunkStart = position;
@@ -60,11 +64,10 @@ export class FileReadActivity extends SyncActivity {
60
64
  const from = Math.max(range.start - chunkStart, 0);
61
65
  const to = Math.min(range.end - chunkStart, chunk.length);
62
66
  chunks.push(chunk.subarray(from, to));
63
- size += to - from;
64
67
  if (position >= range.end)
65
68
  break;
66
69
  }
67
- return { chunks, size };
70
+ return chunks;
68
71
  }
69
72
  }
70
73
  export async function read(binary, options = {}) {