@t4h.framework/fs 0.7.0 → 0.8.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 (33) hide show
  1. package/.ai/skills/framework-fs/SKILL.md +20 -4
  2. package/.ai/skills/framework-fs-write/SKILL.md +242 -0
  3. package/CHANGELOG.md +78 -0
  4. package/LICENSE +21 -0
  5. package/README.md +36 -2
  6. package/dist/FileSystemClaim.d.ts +1 -1
  7. package/dist/FileSystemClaim.d.ts.map +1 -1
  8. package/dist/FileSystemClaim.js.map +1 -1
  9. package/dist/activities/FileWriteActivity.d.ts +20 -0
  10. package/dist/activities/FileWriteActivity.d.ts.map +1 -0
  11. package/dist/activities/FileWriteActivity.js +26 -0
  12. package/dist/activities/FileWriteActivity.js.map +1 -0
  13. package/dist/fs.d.ts +1 -0
  14. package/dist/fs.d.ts.map +1 -1
  15. package/dist/fs.js +1 -0
  16. package/dist/fs.js.map +1 -1
  17. package/package.json +3 -3
  18. package/dist/activities/__tests__/FileJoinActivity.spec.d.ts +0 -2
  19. package/dist/activities/__tests__/FileJoinActivity.spec.d.ts.map +0 -1
  20. package/dist/activities/__tests__/FileJoinActivity.spec.js +0 -323
  21. package/dist/activities/__tests__/FileJoinActivity.spec.js.map +0 -1
  22. package/dist/activities/__tests__/FileReadActivity.spec.d.ts +0 -2
  23. package/dist/activities/__tests__/FileReadActivity.spec.d.ts.map +0 -1
  24. package/dist/activities/__tests__/FileReadActivity.spec.js +0 -120
  25. package/dist/activities/__tests__/FileReadActivity.spec.js.map +0 -1
  26. package/dist/activities/__tests__/FileSplitActivity.spec.d.ts +0 -2
  27. package/dist/activities/__tests__/FileSplitActivity.spec.d.ts.map +0 -1
  28. package/dist/activities/__tests__/FileSplitActivity.spec.js +0 -170
  29. package/dist/activities/__tests__/FileSplitActivity.spec.js.map +0 -1
  30. package/dist/helpers/__tests__/byte-delimiter-scanner.spec.d.ts +0 -2
  31. package/dist/helpers/__tests__/byte-delimiter-scanner.spec.d.ts.map +0 -1
  32. package/dist/helpers/__tests__/byte-delimiter-scanner.spec.js +0 -196
  33. package/dist/helpers/__tests__/byte-delimiter-scanner.spec.js.map +0 -1
@@ -4,6 +4,7 @@ description: >-
4
4
  Guides correct use of @t4h.framework/fs in T4H Framework workflows: the
5
5
  FileSystemClaim abstraction that the package activities (http, jsx) use to
6
6
  read/write files, the Binary result workflows consume, and runtime providers.
7
+ A workflow never implements Binary itself — see the framework-fs-write skill.
7
8
  Use when a workflow orchestrates file-backed package activities, consuming
8
9
  Binary results, mocking FileSystemClaim in workflow tests, registering fs
9
10
  claim providers, or debugging missing fs provider errors.
@@ -28,6 +29,8 @@ import {
28
29
  split, Split, FileSplitActivity, type FileSplitOptions,
29
30
  join, FileJoinActivity, type FileJoinOptions, type FileJoinWhere,
30
31
  read, FileReadActivity, type FileReadOptions,
32
+ // turn workflow content into a Binary — see the framework-fs-write skill
33
+ write, FileWriteActivity, type FileWriteOptions,
31
34
  ByteDelimiterScanner, type FileSplitDelimiter, type ByteRange, type ByteTrim,
32
35
  } from '@t4h.framework/fs'
33
36
  ```
@@ -46,6 +49,7 @@ export abstract class FileSystemClaim {
46
49
  public abstract write(
47
50
  path: string,
48
51
  readable: Readable | Buffer,
52
+ contentType?: string,
49
53
  ): Binary | Promise<Binary>
50
54
 
51
55
  public abstract read(path: string): Binary | Promise<Binary>
@@ -60,7 +64,7 @@ export abstract class FileSystemClaim {
60
64
 
61
65
  | Method | Purpose |
62
66
  |--------|---------|
63
- | `write(path, readable)` | Persist stream or buffer at `path`; return a `Binary` handle (metadata + `stream()`). |
67
+ | `write(path, readable, contentType?)` | Persist stream or buffer at `path`; return a `Binary` handle (metadata + `stream()`). When `contentType` is omitted, infer it from the path. |
64
68
  | `read(path)` | Load an existing file at `path`; return a `Binary` handle. |
65
69
  | `slice(binary, range, encoding?)` | A view over a byte range of an existing binary, **without copying data**. When `encoding` is set, the runtime hydrates the slice as a decoded string instead of a binary — this is how `split` children receive plain strings. Implementations must **compose ranges**: slicing a slice is relative to it, and the stored range is absolute against the underlying file. |
66
70
 
@@ -96,9 +100,15 @@ read the resulting `Binary` (e.g. `await res.body.json()` / `text()`). Your only
96
100
  responsibility is that the runtime **registers a provider** (below). See
97
101
  **Known consumers** for which package primitives to orchestrate.
98
102
 
99
- This package's own workflow-facing primitives are `split`, `join` and `read`
103
+ This package's own workflow-facing primitives are `split`, `join`, `read`
100
104
  (small byte windows — headers, trailers — hydrated into the workflow as a
101
- string, capped at 1 MiB). See the `framework-fs-split` skill.
105
+ string, capped at 1 MiB) and `write` (content produced in the workflow turned
106
+ into a file). See the `framework-fs-split` and `framework-fs-write` skills.
107
+
108
+ **A workflow never implements `Binary`.** Every `Binary` comes from the
109
+ framework — an HTTP response body, `sftp.get`, `write`, `join`. A class holding
110
+ bytes in memory cannot be persisted, and every primitive it is handed to
111
+ persists its input. See the `framework-fs-write` skill.
102
112
 
103
113
  ---
104
114
 
@@ -135,6 +145,9 @@ Test the **workflow** with `TestWorkflowEnvironment` from
135
145
  claims the triggered activities need, e.g. `HttpClientRequestClaim` for an HTTP
136
146
  call). See the **framework-workflow-testing** skill for the harness API.
137
147
 
148
+ Subclassing `Binary` is legitimate **only here**, in a test double standing in
149
+ for the claim — never in workflow or App code.
150
+
138
151
  ```typescript
139
152
  import { Readable } from 'node:stream'
140
153
  import { Binary } from '@t4h.framework/core'
@@ -180,7 +193,9 @@ Assert `mockWrite` was called with the expected path and body when relevant.
180
193
 
181
194
  1. **Extend** `FileSystemClaim` from `@t4h.framework/fs`.
182
195
  2. **Return** a `Binary` subclass (or core-compatible `Binary`) from `write` and `read` with correct `contentType`, `contentLength`, and `stream()`.
183
- 3. **Accept** `Readable | Buffer` in `write` (match the abstract signature).
196
+ 3. **Accept** `Readable | Buffer` and the optional `contentType` in `write`
197
+ (match the abstract signature), falling back to inferring the content type
198
+ from the path when it is omitted.
184
199
  4. **Register** `{ provide: FileSystemClaim, value: new YourImpl() }` in the runtime (tests use `mockClaim(FileSystemClaim, ...)` instead).
185
200
  5. **Keep paths deterministic** when activities replay — unpredictable temp paths are fine if only `Binary` metadata is serialized and replay re-executes `write`; know your serializer/replay story if `path` is persisted.
186
201
 
@@ -206,6 +221,7 @@ Assert `mockWrite` was called with the expected path and body when relevant.
206
221
  ## Related skills
207
222
 
208
223
  - `framework-fs-split` — `split`/`join`: fan a file out into a batch of workflows and fold the results back into a file.
224
+ - `framework-fs-write` — `write`: turn workflow content into a `Binary`, and why an App must never implement `Binary`.
209
225
  - `framework-core` — `Claim`, `Binary`, and the `TestWorkflowEnvironment` testing harness.
210
226
  - `framework-http` — HTTP activities that persist bodies via `FileSystemClaim`.
211
227
  - `framework-jsx` — render activities that write/read via `fs`.
@@ -0,0 +1,242 @@
1
+ ---
2
+ name: framework-fs-write
3
+ description: >-
4
+ Guides the use of `write` from @t4h.framework/fs — the only supported way to
5
+ turn content produced inside a workflow (a JSON string, a CSV, a positional
6
+ return file, a Buffer) into a `Binary` that other primitives accept. Use
7
+ whenever a workflow must upload, send or save content it built itself:
8
+ `sftp.put`/`sftp.append` with generated content, an HTTP request carrying a
9
+ file, persisting a report, or when the user mentions creating/building a
10
+ Binary, converting a string or Buffer to a Binary, "stringToBinary",
11
+ "BufferBinary", or hits `Unsupported binary instance`. A workflow must NEVER
12
+ implement its own `Binary` subclass — this skill explains why and what to do
13
+ instead.
14
+ ---
15
+
16
+ # `write` — turn workflow content into a `Binary`
17
+
18
+ Every file primitive in the framework speaks `Binary`: `sftp.put`,
19
+ `sftp.append`, `split`, `join`, `pdf`, HTTP bodies. A `Binary` is a
20
+ **reference** to stored content, not the bytes themselves.
21
+
22
+ `write` is what produces one from content your workflow built:
23
+
24
+ ```typescript
25
+ import { write } from '@t4h.framework/fs'
26
+
27
+ const file = await write(JSON.stringify(state, null, 2), {
28
+ contentType: 'application/json',
29
+ })
30
+
31
+ await sftp.put('/out/state.json', file)
32
+ ```
33
+
34
+ Package path: `framework/packages/fs`. Import only from the package root.
35
+
36
+ ---
37
+
38
+ ## Never implement `Binary` yourself
39
+
40
+ This is the rule this skill exists to enforce:
41
+
42
+ ```typescript
43
+ // ❌ NEVER — this class cannot be persisted
44
+ class BufferBinary extends Binary {
45
+ constructor(
46
+ private readonly content: Buffer,
47
+ readonly contentType = '...',
48
+ ) {
49
+ super()
50
+ }
51
+
52
+ get contentLength() {
53
+ return this.content.length
54
+ }
55
+
56
+ stream() {
57
+ return Readable.from(this.content)
58
+ }
59
+ }
60
+
61
+ await sftp.put('/out/state.json', new BufferBinary(Buffer.from(json)))
62
+ ```
63
+
64
+ ```typescript
65
+ // ✅ ALWAYS
66
+ await sftp.put(
67
+ '/out/state.json',
68
+ await write(json, {
69
+ contentType: 'application/json',
70
+ }),
71
+ )
72
+ ```
73
+
74
+ ### Why a custom `Binary` cannot work
75
+
76
+ A workflow is **durable**: the body replays, and each activity is persisted
77
+ before it runs. When you pass a value to a primitive, that value becomes the
78
+ activity **input**, and the input is serialized to storage.
79
+
80
+ A `Binary` holding bytes in memory has nothing to serialize into — the buffer
81
+ lives in the process that built it, and the activity may run in another
82
+ process, minutes later, after a replay. The runtime therefore has to spool the
83
+ content somewhere before the activity can be persisted at all.
84
+
85
+ Symptoms of doing it by hand:
86
+
87
+ - `TypeError: Unsupported binary instance` when the activity input is
88
+ serialized, on older runtimes;
89
+ - the activity is never persisted, the transaction rolls back, and the message
90
+ ends up in the dead letter queue after the retries — the run stops mid-way
91
+ with no obvious error in the workflow itself;
92
+ - it may still "work" locally, because the dev server keeps values in memory
93
+ and never crosses a process boundary. **Passing locally proves nothing here.**
94
+
95
+ Current runtimes spool an unknown `Binary` to storage instead of failing, so a
96
+ hand-rolled class may survive — but it pays a hidden copy, loses the content
97
+ type you meant to set, and depends on runtime-version behavior. `write` is the
98
+ contract.
99
+
100
+ ---
101
+
102
+ ## API
103
+
104
+ ```typescript
105
+ function write(
106
+ content: string | Buffer,
107
+ options?: FileWriteOptions,
108
+ ): Promise<Binary>
109
+ ```
110
+
111
+ | Option | Type | Required | Purpose |
112
+ | ------------- | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
113
+ | `contentType` | `string` | No | Content type stored with the file — `'application/json'`, `'text/csv'`, … Defaults to what the filename extension implies, then `application/octet-stream`. |
114
+ | `filename` | `string` | No | Name the file is stored under. Defaults to the activity id. |
115
+ | `encoding` | `BufferEncoding` | No | How a **string** `content` is encoded. Defaults to `'utf8'`. Ignored for a `Buffer`, which always travels as base64. |
116
+
117
+ `write` is a durable activity: on replay the content is **not** written again;
118
+ the recorded `Binary` is returned.
119
+
120
+ ---
121
+
122
+ ## When to use it
123
+
124
+ | Situation | Use |
125
+ | ----------------------------------------------------------------------------- | ----------------------------------------------- |
126
+ | Send content the workflow built (JSON, CSV, positional file) over SFTP/HTTP | `write` |
127
+ | Read a small window of an existing file into the workflow | [`read`](./read) — see **framework-fs-split** |
128
+ | Fan a large file out into one workflow per record | `split` — see **framework-fs-split** |
129
+ | Fold a finished batch back into a file | `join` — see **framework-fs-split** |
130
+ | Content already is a `Binary` (HTTP response body, `sftp.get`, `join` result) | pass it straight through — do not re-`write` it |
131
+
132
+ ---
133
+
134
+ ## Size matters: keep content bounded
135
+
136
+ `write` takes content **in workflow memory**, so it is bounded by what the
137
+ workflow can reasonably hold — a state file, a report, a return file. It is not
138
+ the tool for gigabyte outputs.
139
+
140
+ To build a large file out of many records, use `join` over a batch: it streams
141
+ results one at a time into storage, and nothing but the resulting `Binary`
142
+ reaches the workflow.
143
+
144
+ ---
145
+
146
+ ## Patterns
147
+
148
+ ### A) Write then upload
149
+
150
+ ```typescript
151
+ const csv = await write(rows.map(toCsvLine).join('\n'), {
152
+ contentType: 'text/csv',
153
+ filename: `report-${input.date}.csv`,
154
+ })
155
+
156
+ await sftp.put(`/out/report-${input.date}.csv`, csv)
157
+ ```
158
+
159
+ ### B) Write once, use twice
160
+
161
+ The same `Binary` can be handed to more than one primitive — it is a reference,
162
+ so nothing is copied and nothing is re-uploaded:
163
+
164
+ ```typescript
165
+ const file = await write(json, { contentType: 'application/json' })
166
+
167
+ await sftp.put(`${path}.tmp`, file)
168
+ await sftp.put(path, file)
169
+ ```
170
+
171
+ Note that a hand-rolled `Binary` backed by a single-use stream would break on
172
+ the second call — another reason not to build one.
173
+
174
+ ### C) Binary content
175
+
176
+ ```typescript
177
+ const pdfBytes: Buffer = await buildPdf()
178
+
179
+ await sftp.put(
180
+ '/out/invoice.pdf',
181
+ await write(pdfBytes, {
182
+ contentType: 'application/pdf',
183
+ }),
184
+ )
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Testing
190
+
191
+ `write` goes through `FileSystemClaim.write`, so mock that claim like any
192
+ other. See the **framework-workflow-testing** skill for the harness API.
193
+
194
+ ```typescript
195
+ import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
196
+ import { FileSystemClaim } from '@t4h.framework/fs'
197
+
198
+ // replay runs the workflow twice — return a fresh Binary per call
199
+ const write = vi
200
+ .fn()
201
+ .mockImplementation(
202
+ async (path, content, contentType) =>
203
+ new MockBinary(
204
+ Buffer.isBuffer(content) ? content : await toBuffer(content),
205
+ contentType,
206
+ ),
207
+ )
208
+
209
+ await env.execute(workflow, input, {
210
+ claims: [
211
+ mockClaim(FileSystemClaim, { write, read: vi.fn(), slice: vi.fn() }),
212
+ ],
213
+ })
214
+
215
+ expect(write).toHaveBeenCalledWith('report.csv', expect.any(Buffer), 'text/csv')
216
+ ```
217
+
218
+ ---
219
+
220
+ ## Anti-patterns
221
+
222
+ - **Declaring a class that extends `Binary`** in an App. There is no valid
223
+ reason to; use `write`.
224
+ - Helper functions like `stringToBinary(value)` that wrap content in a custom
225
+ class — replace the implementation with `write`, keeping the helper name if
226
+ it reads well.
227
+ - `write`-ing a value that already is a `Binary` (an HTTP response body, a
228
+ `sftp.get` result, a `join` output) — that copies a file to obtain a
229
+ reference you already had.
230
+ - Building a large file by concatenating records in memory and calling `write`
231
+ once — use `split`/`join` instead.
232
+ - Relying on the filename extension to set the content type when the consumer
233
+ cares about it — pass `contentType` explicitly.
234
+
235
+ ---
236
+
237
+ ## Related skills
238
+
239
+ - `framework-fs` — the `FileSystemClaim` contract and runtime providers.
240
+ - `framework-fs-split` — `read`/`split`/`join`: windows, fan-out and fold.
241
+ - `framework-sftp` — `sftp.put`/`append`, the most common consumer of `write`.
242
+ - `framework-workflow-creation` — the rules a workflow body must follow.
package/CHANGELOG.md ADDED
@@ -0,0 +1,78 @@
1
+ # @t4h.framework/fs
2
+
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`8c56f76`](https://github.com/tech4humans-brasil/framework/commit/8c56f7697390a1ea966d1e91ff5f35ce5751a0d7) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - feat: write in-memory content to durable storage from a workflow
8
+
9
+ There was no supported way to turn a string or a Buffer into a `Binary` inside
10
+ a workflow — every `Binary` came from an existing file (`http` response,
11
+ `sftp.get`, `split`, `join`). Anyone needing to upload computed content had to
12
+ implement `Binary` by hand, and such an instance can never be persisted, so the
13
+ activity carrying it failed to serialize.
14
+
15
+ `write(content, options?)` covers that gap: it pushes an activity that stores
16
+ the content through the `FileSystemClaim` and returns a `Binary` that survives
17
+ replay.
18
+
19
+ ```ts
20
+ const file = await write(JSON.stringify(data), {
21
+ contentType: 'application/json',
22
+ })
23
+
24
+ await sftp.put('/out/data.json', file)
25
+ ```
26
+
27
+ `FileSystemClaim.write` takes an optional `contentType` — until now the content
28
+ type was always inferred from the file extension.
29
+
30
+ The dev codec no longer diverges from the runtime one: its binary kind used to
31
+ match only `DevFileBinary`, so a hand-written `Binary` was silently serialized
32
+ as a plain object locally and blew up once deployed. It now matches any
33
+ `Binary` and spools unknown ones to disk, the same way the runtime spools them
34
+ to the file storage.
35
+
36
+ - [`8c56f76`](https://github.com/tech4humans-brasil/framework/commit/8c56f7697390a1ea966d1e91ff5f35ce5751a0d7) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Adds FS write activity
37
+
38
+ ## 0.7.0
39
+
40
+ ### Minor Changes
41
+
42
+ - Adds support to read file ([#109](https://github.com/tech4humans-brasil/framework/pull/109))
43
+
44
+ ## 0.6.0
45
+
46
+ ### Minor Changes
47
+
48
+ - Adds activity to split and join binary ([#100](https://github.com/tech4humans-brasil/framework/pull/100))
49
+
50
+ ## 0.5.0
51
+
52
+ ### Patch Changes
53
+
54
+ - Update skills ([`6d7e7bc`](https://github.com/tech4humans-brasil/framework/commit/6d7e7bca0c780a9c53a0c9e5aa0a865c5af67959))
55
+
56
+ ## 0.4.0
57
+
58
+ ### Patch Changes
59
+
60
+ - Internal dependency updates
61
+
62
+ ## 0.3.0
63
+
64
+ ### Minor Changes
65
+
66
+ - Adds fs package ([`538c9bd`](https://github.com/tech4humans-brasil/framework/commit/538c9bd5c49031ceb94ef8d6c513f6ffc9c848c5))
67
+ - Adds support to buffer in fs write package ([`476f9f0`](https://github.com/tech4humans-brasil/framework/commit/476f9f08a7b379a3361e42f8577078462276210b))
68
+ - Adds .ai/skills in packages ([#88](https://github.com/tech4humans-brasil/framework/pull/88))
69
+
70
+ ### Patch Changes
71
+
72
+ - Remove activity prefix from file system claim class name ([`d257bd1`](https://github.com/tech4humans-brasil/framework/commit/d257bd177d477a56a897ba862a0be1f50531c633))
73
+ - Adds github workflow to run, build, tests and lint ([#18](https://github.com/tech4humans-brasil/framework/pull/18))
74
+ - Adds readme ([`3a55794`](https://github.com/tech4humans-brasil/framework/commit/3a557942769fa022b9a39504046d22b0083e734f))
75
+ - Update package metadata and author information across multiple packages ([`2ca73df`](https://github.com/tech4humans-brasil/framework/commit/2ca73df42dc63c86ed44717b50d5402fca45b4e7))
76
+ - Improve repository info ([`1fad92d`](https://github.com/tech4humans-brasil/framework/commit/1fad92d959b804189fd1486d047115995ac81eb0))
77
+ - Fix resolve module resolution issues in check-types ([`c97a0d2`](https://github.com/tech4humans-brasil/framework/commit/c97a0d2747f38e561cc5c7d592b20de0459bc1ee))
78
+ - Update package dependencies across multiple packages ([`8b4fc1d`](https://github.com/tech4humans-brasil/framework/commit/8b4fc1d0e92d6bd880ddf4c8119fb68c49c53760))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tech for humans
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  File system claim for reading and writing files within activities, plus the
4
4
  workflow-facing file activities: `split` fans a large file out into one child
5
5
  workflow per slice, `join` folds the batch results back into a single file,
6
- and `read` brings a small byte window (a header, a trailer) into the workflow
7
- as a string.
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.
8
8
 
9
9
  ## Claim
10
10
 
@@ -23,6 +23,7 @@ abstract class FileSystemClaim {
23
23
  public abstract write(
24
24
  path: string,
25
25
  readable: Readable | Buffer,
26
+ contentType?: string,
26
27
  ): Binary | Promise<Binary>
27
28
  public abstract read(path: string): Binary | Promise<Binary>
28
29
 
@@ -107,6 +108,39 @@ Reading a slice is relative to the slice: `read(slice, { range: { start: 0,
107
108
  end: 10 } })` returns the first 10 bytes of the slice, not of the underlying
108
109
  file — slice ranges compose.
109
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`:
117
+
118
+ ```typescript
119
+ import { write } from '@t4h.framework/fs'
120
+
121
+ const file = await write(JSON.stringify(state, null, 2), {
122
+ contentType: 'application/json',
123
+ })
124
+
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
+
110
144
  ## File split
111
145
 
112
146
  ### split
@@ -1,7 +1,7 @@
1
1
  import type { Readable } from 'node:stream';
2
2
  import type { Binary } from '@t4h.framework/core';
3
3
  export declare abstract class FileSystemClaim {
4
- abstract write(path: string, readable: Readable | Buffer): Binary | Promise<Binary>;
4
+ abstract write(path: string, readable: Readable | Buffer, contentType?: string): Binary | Promise<Binary>;
5
5
  abstract read(path: string): Binary | Promise<Binary>;
6
6
  abstract slice(binary: Binary, range: {
7
7
  start: number;
@@ -1 +1 @@
1
- {"version":3,"file":"FileSystemClaim.d.ts","sourceRoot":"","sources":["../src/FileSystemClaim.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAEjD,8BAAsB,eAAe;aACnB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAC1B,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;aAEX,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;aAE5C,KAAK,CACnB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EACrC,QAAQ,CAAC,EAAE,cAAc,GACxB,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAC5B"}
1
+ {"version":3,"file":"FileSystemClaim.d.ts","sourceRoot":"","sources":["../src/FileSystemClaim.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAEjD,8BAAsB,eAAe;aACnB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,GAAG,MAAM,EAC3B,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;aAEX,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;aAE5C,KAAK,CACnB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EACrC,QAAQ,CAAC,EAAE,cAAc,GACxB,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAC5B"}
@@ -1 +1 @@
1
- {"version":3,"file":"FileSystemClaim.js","sourceRoot":"","sources":["../src/FileSystemClaim.ts"],"names":[],"mappings":"AAIA,MAAM,OAAgB,eAAe;CAapC"}
1
+ {"version":3,"file":"FileSystemClaim.js","sourceRoot":"","sources":["../src/FileSystemClaim.ts"],"names":[],"mappings":"AAIA,MAAM,OAAgB,eAAe;CAcpC"}
@@ -0,0 +1,20 @@
1
+ import { type Binary, SyncActivity } from '@t4h.framework/core';
2
+ export type FileWriteOptions = {
3
+ filename?: string;
4
+ contentType?: string;
5
+ encoding?: BufferEncoding;
6
+ };
7
+ export type FileWriteActivityInput = {
8
+ content: string;
9
+ encoding: BufferEncoding;
10
+ filename: string;
11
+ contentType?: string;
12
+ };
13
+ export declare class FileWriteActivity extends SyncActivity<readonly [content: string | Buffer, options: FileWriteOptions], FileWriteActivityInput, Binary, Binary> {
14
+ private readonly claims;
15
+ toInput(content: string | Buffer, options: FileWriteOptions): FileWriteActivityInput;
16
+ run({ content, encoding, filename, contentType, }: FileWriteActivityInput): Promise<Binary>;
17
+ toOutput(output: Binary): Binary;
18
+ }
19
+ export declare function write(content: string | Buffer, options?: FileWriteOptions): Promise<Binary>;
20
+ //# sourceMappingURL=FileWriteActivity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileWriteActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileWriteActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAI/E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,qBAAa,iBAAkB,SAAQ,YAAY,CACjD,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,gBAAgB,CAAC,EAC9D,sBAAsB,EACtB,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAErB;IAEK,OAAO,CACZ,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,EAAE,gBAAgB,GACxB,sBAAsB;IAWZ,GAAG,CAAC,EACf,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,WAAW,GACZ,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC;IAQpC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;CAGxC;AAED,wBAAsB,KAAK,CACzB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,CAEjB"}
@@ -0,0 +1,26 @@
1
+ import { Claim, History, SyncActivity } from '@t4h.framework/core';
2
+ import { FileSystemClaim } from '../FileSystemClaim.js';
3
+ export class FileWriteActivity extends SyncActivity {
4
+ claims = new Claim({
5
+ fs: FileSystemClaim,
6
+ });
7
+ toInput(content, options) {
8
+ const isBuffer = Buffer.isBuffer(content);
9
+ return {
10
+ content: isBuffer ? content.toString('base64') : content,
11
+ encoding: isBuffer ? 'base64' : (options.encoding ?? 'utf8'),
12
+ filename: options.filename ?? this.id,
13
+ contentType: options.contentType,
14
+ };
15
+ }
16
+ async run({ content, encoding, filename, contentType, }) {
17
+ return await this.claims.fs.write(filename, Buffer.from(content, encoding), contentType);
18
+ }
19
+ toOutput(output) {
20
+ return output;
21
+ }
22
+ }
23
+ export async function write(content, options = {}) {
24
+ return await History.reconciler(FileWriteActivity, content, options);
25
+ }
26
+ //# sourceMappingURL=FileWriteActivity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileWriteActivity.js","sourceRoot":"","sources":["../../src/activities/FileWriteActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAE/E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAevD,MAAM,OAAO,iBAAkB,SAAQ,YAKtC;IACkB,MAAM,GAAG,IAAI,KAAK,CAAC;QAClC,EAAE,EAAE,eAAe;KACpB,CAAC,CAAA;IAEK,OAAO,CACZ,OAAwB,EACxB,OAAyB;QAEzB,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;QAEzC,OAAO;YACL,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO;YACxD,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC;YAC5D,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC,EAAE;YACrC,WAAW,EAAE,OAAO,CAAC,WAAW;SACjC,CAAA;IACH,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,EACf,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,WAAW,GACY;QACvB,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAC/B,QAAQ,EACR,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,EAC9B,WAAW,CACZ,CAAA;IACH,CAAC;IAEM,QAAQ,CAAC,MAAc;QAC5B,OAAO,MAAM,CAAA;IACf,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,OAAwB,EACxB,UAA4B,EAAE;IAE9B,OAAO,MAAM,OAAO,CAAC,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;AACtE,CAAC"}
package/dist/fs.d.ts CHANGED
@@ -2,5 +2,6 @@ export * from './FileSystemClaim.js';
2
2
  export * from './activities/FileJoinActivity.js';
3
3
  export * from './activities/FileReadActivity.js';
4
4
  export * from './activities/FileSplitActivity.js';
5
+ export * from './activities/FileWriteActivity.js';
5
6
  export * from './helpers/byte-delimiter-scanner.js';
6
7
  //# sourceMappingURL=fs.d.ts.map
package/dist/fs.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,kCAAkC,CAAA;AAChD,cAAc,kCAAkC,CAAA;AAChD,cAAc,mCAAmC,CAAA;AACjD,cAAc,qCAAqC,CAAA"}
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,kCAAkC,CAAA;AAChD,cAAc,kCAAkC,CAAA;AAChD,cAAc,mCAAmC,CAAA;AACjD,cAAc,mCAAmC,CAAA;AACjD,cAAc,qCAAqC,CAAA"}
package/dist/fs.js CHANGED
@@ -2,5 +2,6 @@ export * from './FileSystemClaim.js';
2
2
  export * from './activities/FileJoinActivity.js';
3
3
  export * from './activities/FileReadActivity.js';
4
4
  export * from './activities/FileSplitActivity.js';
5
+ export * from './activities/FileWriteActivity.js';
5
6
  export * from './helpers/byte-delimiter-scanner.js';
6
7
  //# sourceMappingURL=fs.js.map
package/dist/fs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,kCAAkC,CAAA;AAChD,cAAc,kCAAkC,CAAA;AAChD,cAAc,mCAAmC,CAAA;AACjD,cAAc,qCAAqC,CAAA"}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,kCAAkC,CAAA;AAChD,cAAc,kCAAkC,CAAA;AAChD,cAAc,mCAAmC,CAAA;AACjD,cAAc,mCAAmC,CAAA;AACjD,cAAc,qCAAqC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/fs",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "File system module for the T4H Framework",
5
5
  "homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/fs",
6
6
  "bugs": "https://github.com/tech4humans-brasil/framework/issues",
@@ -32,12 +32,12 @@
32
32
  "test:watch": "vitest"
33
33
  },
34
34
  "devDependencies": {
35
- "@t4h.framework/core": "^0.9.0",
35
+ "@t4h.framework/core": "^0.9.1",
36
36
  "typescript": "^5.9.3",
37
37
  "vitest": "^4.0.18"
38
38
  },
39
39
  "peerDependencies": {
40
- "@t4h.framework/core": "^0.9.0"
40
+ "@t4h.framework/core": "^0.9.1"
41
41
  },
42
42
  "packageManager": "yarn@4.12.0",
43
43
  "engines": {
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=FileJoinActivity.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FileJoinActivity.spec.d.ts","sourceRoot":"","sources":["../../../src/activities/__tests__/FileJoinActivity.spec.ts"],"names":[],"mappings":""}