@t4h.framework/fs 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai/skills/framework-fs/SKILL.md +72 -115
- package/.ai/skills/framework-fs-split/SKILL.md +255 -0
- package/README.md +184 -3
- package/dist/FileSystemClaim.d.ts +4 -0
- package/dist/FileSystemClaim.d.ts.map +1 -1
- package/dist/FileSystemClaim.js.map +1 -1
- package/dist/activities/FileJoinActivity.d.ts +39 -0
- package/dist/activities/FileJoinActivity.d.ts.map +1 -0
- package/dist/activities/FileJoinActivity.js +87 -0
- package/dist/activities/FileJoinActivity.js.map +1 -0
- package/dist/activities/FileSplitActivity.d.ts +38 -0
- package/dist/activities/FileSplitActivity.d.ts.map +1 -0
- package/dist/activities/FileSplitActivity.js +51 -0
- package/dist/activities/FileSplitActivity.js.map +1 -0
- package/dist/activities/__tests__/FileJoinActivity.spec.d.ts +2 -0
- package/dist/activities/__tests__/FileJoinActivity.spec.d.ts.map +1 -0
- package/dist/activities/__tests__/FileJoinActivity.spec.js +160 -0
- package/dist/activities/__tests__/FileJoinActivity.spec.js.map +1 -0
- package/dist/activities/__tests__/FileSplitActivity.spec.d.ts +2 -0
- package/dist/activities/__tests__/FileSplitActivity.spec.d.ts.map +1 -0
- package/dist/activities/__tests__/FileSplitActivity.spec.js +149 -0
- package/dist/activities/__tests__/FileSplitActivity.spec.js.map +1 -0
- package/dist/fs.d.ts +3 -0
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +3 -0
- package/dist/fs.js.map +1 -1
- package/dist/helpers/__tests__/byte-delimiter-scanner.spec.d.ts +2 -0
- package/dist/helpers/__tests__/byte-delimiter-scanner.spec.d.ts.map +1 -0
- package/dist/helpers/__tests__/byte-delimiter-scanner.spec.js +127 -0
- package/dist/helpers/__tests__/byte-delimiter-scanner.spec.js.map +1 -0
- package/dist/helpers/__tests__/csv-reader.spec.d.ts +2 -0
- package/dist/helpers/__tests__/csv-reader.spec.d.ts.map +1 -0
- package/dist/helpers/__tests__/csv-reader.spec.js +60 -0
- package/dist/helpers/__tests__/csv-reader.spec.js.map +1 -0
- package/dist/helpers/byte-delimiter-scanner.d.ts +36 -0
- package/dist/helpers/byte-delimiter-scanner.d.ts.map +1 -0
- package/dist/helpers/byte-delimiter-scanner.js +61 -0
- package/dist/helpers/byte-delimiter-scanner.js.map +1 -0
- package/dist/helpers/csv-reader.d.ts +8 -0
- package/dist/helpers/csv-reader.d.ts.map +1 -0
- package/dist/helpers/csv-reader.js +14 -0
- package/dist/helpers/csv-reader.js.map +1 -0
- package/dist/helpers/parsers.d.ts +9 -0
- package/dist/helpers/parsers.d.ts.map +1 -0
- package/dist/helpers/parsers.js +14 -0
- package/dist/helpers/parsers.js.map +1 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @t4h.framework/fs
|
|
2
2
|
|
|
3
|
-
File system claim for reading and writing files within activities
|
|
3
|
+
File system claim for reading and writing files within activities, and a file
|
|
4
|
+
split activity that fans a large file out into one child workflow per slice.
|
|
4
5
|
|
|
5
6
|
## Claim
|
|
6
7
|
|
|
@@ -16,8 +17,17 @@ import { FileSystemClaim } from '@t4h.framework/fs'
|
|
|
16
17
|
|
|
17
18
|
```typescript
|
|
18
19
|
abstract class FileSystemClaim {
|
|
19
|
-
public abstract write(
|
|
20
|
+
public abstract write(
|
|
21
|
+
path: string,
|
|
22
|
+
readable: Readable | Buffer,
|
|
23
|
+
): Binary | Promise<Binary>
|
|
20
24
|
public abstract read(path: string): Binary | Promise<Binary>
|
|
25
|
+
|
|
26
|
+
public abstract slice(
|
|
27
|
+
binary: Binary,
|
|
28
|
+
range: { start: number; end: number },
|
|
29
|
+
encoding?: BufferEncoding,
|
|
30
|
+
): Binary | Promise<Binary>
|
|
21
31
|
}
|
|
22
32
|
```
|
|
23
33
|
|
|
@@ -27,7 +37,11 @@ abstract class FileSystemClaim {
|
|
|
27
37
|
import { SyncActivity, Claim } from '@t4h.framework/core'
|
|
28
38
|
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
29
39
|
|
|
30
|
-
class SaveReportActivity extends SyncActivity<
|
|
40
|
+
class SaveReportActivity extends SyncActivity<
|
|
41
|
+
[data: Buffer],
|
|
42
|
+
{ path: string },
|
|
43
|
+
any
|
|
44
|
+
> {
|
|
31
45
|
private readonly claims = new Claim({
|
|
32
46
|
fs: FileSystemClaim,
|
|
33
47
|
})
|
|
@@ -54,3 +68,170 @@ The runtime must supply a concrete implementation:
|
|
|
54
68
|
```typescript
|
|
55
69
|
{ provide: FileSystemClaim, value: new LocalFileSystemClaimImpl() }
|
|
56
70
|
```
|
|
71
|
+
|
|
72
|
+
## File split
|
|
73
|
+
|
|
74
|
+
### split
|
|
75
|
+
|
|
76
|
+
Splits a file into byte-range slices, starts one child workflow per slice as
|
|
77
|
+
a single batch, and suspends the parent until every child is terminal —
|
|
78
|
+
regardless of each child succeeding or failing (a rejected child never
|
|
79
|
+
rejects the batch):
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { split, Split } from '@t4h.framework/fs'
|
|
83
|
+
|
|
84
|
+
const processRecord = new Workflow(
|
|
85
|
+
{ id: 'process-record', schema: Type.String() },
|
|
86
|
+
async (record: string) => {
|
|
87
|
+
// record is the raw slice content
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const main = new Workflow(fileSchema, async (file: Binary) => {
|
|
92
|
+
const batch = await split(file, processRecord, {
|
|
93
|
+
delimiter: Split.line(),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// this line only runs once the whole batch is terminal;
|
|
97
|
+
// batch: WorkflowBatchRef — { id: string, count: number }
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The design goal is constant memory regardless of file size:
|
|
102
|
+
|
|
103
|
+
1. The source stream is read **exactly once**, worker side. The same pass
|
|
104
|
+
persists a durable copy through `FileSystemClaim.write` while a byte
|
|
105
|
+
scanner records the slice offsets — no slice content is ever materialized
|
|
106
|
+
in the activity.
|
|
107
|
+
2. Children receive **range views** over the durable copy
|
|
108
|
+
(`FileSystemClaim.slice`).
|
|
109
|
+
3. The activity output is just the batch reference (`{ id, count }`), so the
|
|
110
|
+
parent history stays O(1) whatever the slice count. Per-child results are
|
|
111
|
+
never aggregated: they live with each child run, to be inspected
|
|
112
|
+
individually through the batch id.
|
|
113
|
+
|
|
114
|
+
Child workflows declare their real input type (e.g. `Type.String()`); how the
|
|
115
|
+
bytes reach them is a runtime concern.
|
|
116
|
+
|
|
117
|
+
### Delimiters
|
|
118
|
+
|
|
119
|
+
The cut is defined by the dev through a serializable delimiter spec. When the
|
|
120
|
+
delimiter is found at `[p, p + length)`, the previous slice ends at
|
|
121
|
+
`p + keep.previous` and the next slice starts at `p + length - keep.next`; the
|
|
122
|
+
bytes in between are discarded. The defaults discard the whole delimiter
|
|
123
|
+
(classic line semantics).
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
Split.line() // cut at every '\n', delimiter discarded
|
|
127
|
+
Split.char(';') // cut at any byte sequence
|
|
128
|
+
Split.char(';', { previous: 1 }) // keep the ';' with the previous slice
|
|
129
|
+
Split.char('\n10', { next: 2 }) // positional files, see below
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The last form targets positional files where a logical record spans multiple
|
|
133
|
+
physical lines and a new record only starts when a line begins with a marker
|
|
134
|
+
(the record type code):
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
10JOHN;123\n20detail-a\n20detail-b\n10MARY;456\n20detail-c
|
|
138
|
+
└──────────── record 1 ───────────┘└─────── record 2 ──────┘
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
With `Split.char('\n10', { next: 2 })` the newline between records is
|
|
142
|
+
discarded, and the `10` marker — which is data, not separator — stays at the
|
|
143
|
+
start of the next slice.
|
|
144
|
+
|
|
145
|
+
### Options
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
await split(file, workflow, {
|
|
149
|
+
delimiter: Split.line(), // required: where to cut
|
|
150
|
+
every: 10_000, // cut only every N occurrences (groups records)
|
|
151
|
+
skipEmpty: false, // keep zero-length slices (default: skipped)
|
|
152
|
+
encoding: 'latin1', // delimiter bytes + slice hydration (default: utf8)
|
|
153
|
+
timeout: 3_600_000, // max ms to await the batch; rejects on expiry
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- **`every`** groups N records per child. With one million lines, `every:
|
|
158
|
+
10_000` starts 100 children instead of one million; each child receives a
|
|
159
|
+
raw block with its inner delimiters untouched and iterates locally. `keep`
|
|
160
|
+
arithmetic only applies at actual cut points.
|
|
161
|
+
- **`skipEmpty`** (default `true`) drops zero-length slices — consecutive
|
|
162
|
+
delimiters, or a file starting/ending with one — so they never become child
|
|
163
|
+
workflows. Turn it off when the slice index must align 1:1 with the record
|
|
164
|
+
position in the source file. "Empty" means zero bytes: a whitespace-only
|
|
165
|
+
line is a regular slice.
|
|
166
|
+
- **`encoding`** converts the delimiter to bytes and tells the runtime how to
|
|
167
|
+
hydrate each slice for the child. Offsets are always computed over raw
|
|
168
|
+
bytes, so multi-byte encodings never skew the ranges.
|
|
169
|
+
- **`timeout`** is the maximum time in milliseconds to await the batch. On
|
|
170
|
+
expiry the activity rejects (reason `timeout`) instead of suspending
|
|
171
|
+
forever — the safety net for a child that never reaches a terminal state.
|
|
172
|
+
|
|
173
|
+
## File join
|
|
174
|
+
|
|
175
|
+
The inverse of `split`: folds the results of a completed batch into a single
|
|
176
|
+
file. Outputs stream in one at a time through `WorkflowBatchClaim` and flow
|
|
177
|
+
straight into `FileSystemClaim.write` with backpressure, so memory stays
|
|
178
|
+
bounded whatever the batch size — only the resulting binary reaches the
|
|
179
|
+
workflow history.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { join, split, Split } from '@t4h.framework/fs'
|
|
183
|
+
|
|
184
|
+
const batch = await split(file, processRecord, { delimiter: Split.line() })
|
|
185
|
+
|
|
186
|
+
const csv = await join(batch, {
|
|
187
|
+
format: 'csv',
|
|
188
|
+
columns: [
|
|
189
|
+
{ key: 'id', header: 'Código' },
|
|
190
|
+
{ key: 'value', header: 'Valor (R$)' },
|
|
191
|
+
'date', // shorthand: key === header
|
|
192
|
+
],
|
|
193
|
+
})
|
|
194
|
+
// csv: Binary
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Formats
|
|
198
|
+
|
|
199
|
+
- **`lines`** — concatenates raw child outputs; the child owns the line
|
|
200
|
+
format. Options: `header`, `separator` (default `'\n'`).
|
|
201
|
+
- **`entries`** — NDJSON dump of `{ index, status, output }`, one entry per
|
|
202
|
+
line. The format-agnostic view of the batch, rejected children included by
|
|
203
|
+
default.
|
|
204
|
+
- **`csv`** — projects object outputs into RFC 4180 escaped columns. Options:
|
|
205
|
+
`columns` (required; each entry is `{ key, header? }` where `key` selects
|
|
206
|
+
the value on the child output and `header` is the label on the header row —
|
|
207
|
+
a plain string is shorthand for `{ key }`), `delimiter` (default `','`),
|
|
208
|
+
`header` (default `true`; emits the column labels as the first row).
|
|
209
|
+
|
|
210
|
+
### Status filter
|
|
211
|
+
|
|
212
|
+
`status` selects which child results make it into the file, instead of an
|
|
213
|
+
error policy: it defaults to `['fulfilled']` for `lines` and `csv`, and to
|
|
214
|
+
both statuses for `entries`. Filtering is silent by design — a file of
|
|
215
|
+
rejected children for reprocessing is one call away:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const failures = await join(batch, { format: 'entries', status: ['rejected'] })
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### ByteDelimiterScanner
|
|
222
|
+
|
|
223
|
+
The scanner behind `split` is exported for direct use. It is a pass-through
|
|
224
|
+
`Transform`: chunks flow through unchanged (so the same pass can persist the
|
|
225
|
+
file) while it records slice offsets in absolute byte positions, handling
|
|
226
|
+
delimiters split across chunk boundaries. `ranges` is complete once the stream
|
|
227
|
+
has flushed.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { ByteDelimiterScanner } from '@t4h.framework/fs'
|
|
231
|
+
|
|
232
|
+
const scanner = new ByteDelimiterScanner({ delimiter: { value: '\n' } })
|
|
233
|
+
|
|
234
|
+
await pipeline(source, scanner, destination)
|
|
235
|
+
|
|
236
|
+
scanner.ranges // [{ start: 0, end: 3 }, ...]
|
|
237
|
+
```
|
|
@@ -3,5 +3,9 @@ import type { Binary } from '@t4h.framework/core';
|
|
|
3
3
|
export declare abstract class FileSystemClaim {
|
|
4
4
|
abstract write(path: string, readable: Readable | Buffer): Binary | Promise<Binary>;
|
|
5
5
|
abstract read(path: string): Binary | Promise<Binary>;
|
|
6
|
+
abstract slice(binary: Binary, range: {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
}, encoding?: BufferEncoding): Binary | Promise<Binary>;
|
|
6
10
|
}
|
|
7
11
|
//# sourceMappingURL=FileSystemClaim.d.ts.map
|
|
@@ -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;
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileSystemClaim.js","sourceRoot":"","sources":["../src/FileSystemClaim.ts"],"names":[],"mappings":"AAIA,MAAM,OAAgB,eAAe;
|
|
1
|
+
{"version":3,"file":"FileSystemClaim.js","sourceRoot":"","sources":["../src/FileSystemClaim.ts"],"names":[],"mappings":"AAIA,MAAM,OAAgB,eAAe;CAapC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Binary, SyncActivity, type WorkflowBatchRef } from '@t4h.framework/core';
|
|
2
|
+
export type FileJoinStatus = 'fulfilled' | 'rejected';
|
|
3
|
+
type FileJoinOptionsBase = {
|
|
4
|
+
status?: FileJoinStatus[];
|
|
5
|
+
};
|
|
6
|
+
type FileJoinOptionsLines = FileJoinOptionsBase & {
|
|
7
|
+
format: 'lines';
|
|
8
|
+
header?: string;
|
|
9
|
+
separator?: string;
|
|
10
|
+
};
|
|
11
|
+
type FileJoinOptionsEntries = FileJoinOptionsBase & {
|
|
12
|
+
format: 'entries';
|
|
13
|
+
};
|
|
14
|
+
export type FileJoinColumn = string | {
|
|
15
|
+
key: string;
|
|
16
|
+
header?: string;
|
|
17
|
+
};
|
|
18
|
+
type FileJoinOptionsCsv = FileJoinOptionsBase & {
|
|
19
|
+
format: 'csv';
|
|
20
|
+
columns: FileJoinColumn[];
|
|
21
|
+
delimiter?: string;
|
|
22
|
+
header?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type FileJoinOptions = FileJoinOptionsLines | FileJoinOptionsEntries | FileJoinOptionsCsv;
|
|
25
|
+
export type FileJoinActivityInput = {
|
|
26
|
+
batch: string;
|
|
27
|
+
options: FileJoinOptions;
|
|
28
|
+
};
|
|
29
|
+
export declare class FileJoinActivity extends SyncActivity<readonly [batch: WorkflowBatchRef, options: FileJoinOptions], FileJoinActivityInput, Binary, Binary> {
|
|
30
|
+
private readonly claims;
|
|
31
|
+
toInput(batch: WorkflowBatchRef, options: FileJoinOptions): FileJoinActivityInput;
|
|
32
|
+
run({ batch, options }: FileJoinActivityInput): Promise<Binary>;
|
|
33
|
+
toOutput(output: Binary): Binary;
|
|
34
|
+
private writeHeader;
|
|
35
|
+
private toLine;
|
|
36
|
+
}
|
|
37
|
+
export declare function join(batch: WorkflowBatchRef, options: FileJoinOptions): Promise<Binary>;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=FileJoinActivity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileJoinActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,MAAM,EAGX,YAAY,EAGZ,KAAK,gBAAgB,EACtB,MAAM,qBAAqB,CAAA;AAI5B,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,UAAU,CAAA;AAErD,KAAK,mBAAmB,GAAG;IACzB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;CAC1B,CAAA;AAED,KAAK,oBAAoB,GAAG,mBAAmB,GAAG;IAChD,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,KAAK,sBAAsB,GAAG,mBAAmB,GAAG;IAClD,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,cAAc,GACtB,MAAM,GACN;IACE,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAEL,KAAK,kBAAkB,GAAG,mBAAmB,GAAG;IAC9C,MAAM,EAAE,KAAK,CAAA;IACb,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GACvB,oBAAoB,GACpB,sBAAsB,GACtB,kBAAkB,CAAA;AAEtB,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,eAAe,CAAA;CACzB,CAAA;AAgCD,qBAAa,gBAAiB,SAAQ,YAAY,CAChD,SAAS,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,eAAe,CAAC,EAC5D,qBAAqB,EACrB,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGrB;IAEK,OAAO,CACZ,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,eAAe,GACvB,qBAAqB;IAIX,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAgCrE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;YAIzB,WAAW;IAkBzB,OAAO,CAAC,MAAM;CAsBf;AAED,wBAAsB,IAAI,CACxB,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,MAAM,CAAC,CAEjB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { once } from 'node:events';
|
|
2
|
+
import { PassThrough } from 'node:stream';
|
|
3
|
+
import { Claim, History, SyncActivity, WorkflowBatchClaim, } from '@t4h.framework/core';
|
|
4
|
+
import { FileSystemClaim } from '../FileSystemClaim.js';
|
|
5
|
+
const extensions = {
|
|
6
|
+
lines: 'txt',
|
|
7
|
+
entries: 'ndjson',
|
|
8
|
+
csv: 'csv',
|
|
9
|
+
};
|
|
10
|
+
function columnKey(column) {
|
|
11
|
+
return typeof column === 'string' ? column : column.key;
|
|
12
|
+
}
|
|
13
|
+
function columnHeader(column) {
|
|
14
|
+
return typeof column === 'string' ? column : (column.header ?? column.key);
|
|
15
|
+
}
|
|
16
|
+
function csvEscape(value, delimiter) {
|
|
17
|
+
if (value === null || typeof value === 'undefined')
|
|
18
|
+
return '';
|
|
19
|
+
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
20
|
+
if (text.includes(delimiter) ||
|
|
21
|
+
text.includes('"') ||
|
|
22
|
+
text.includes('\n') ||
|
|
23
|
+
text.includes('\r'))
|
|
24
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
export class FileJoinActivity extends SyncActivity {
|
|
28
|
+
claims = new Claim({
|
|
29
|
+
fs: FileSystemClaim,
|
|
30
|
+
batch: WorkflowBatchClaim,
|
|
31
|
+
});
|
|
32
|
+
toInput(batch, options) {
|
|
33
|
+
return { batch: batch.id, options };
|
|
34
|
+
}
|
|
35
|
+
async run({ batch, options }) {
|
|
36
|
+
const stream = new PassThrough();
|
|
37
|
+
const writing = this.claims.fs.write(`join/${this.id}.${extensions[options.format]}`, stream);
|
|
38
|
+
const write = async (line) => {
|
|
39
|
+
if (!stream.write(line))
|
|
40
|
+
await once(stream, 'drain');
|
|
41
|
+
};
|
|
42
|
+
const statuses = new Set(options.status ??
|
|
43
|
+
(options.format === 'entries'
|
|
44
|
+
? ['fulfilled', 'rejected']
|
|
45
|
+
: ['fulfilled']));
|
|
46
|
+
await this.writeHeader(options, write);
|
|
47
|
+
for await (const entry of this.claims.batch.outputs(batch)) {
|
|
48
|
+
if (!statuses.has(entry.status))
|
|
49
|
+
continue;
|
|
50
|
+
await write(this.toLine(entry, options));
|
|
51
|
+
}
|
|
52
|
+
stream.end();
|
|
53
|
+
return await writing;
|
|
54
|
+
}
|
|
55
|
+
toOutput(output) {
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
async writeHeader(options, write) {
|
|
59
|
+
if (options.format === 'lines' && options.header)
|
|
60
|
+
await write(options.header + (options.separator ?? '\n'));
|
|
61
|
+
if (options.format === 'csv' && options.header !== false) {
|
|
62
|
+
const delimiter = options.delimiter ?? ',';
|
|
63
|
+
await write(options.columns
|
|
64
|
+
.map(column => csvEscape(columnHeader(column), delimiter))
|
|
65
|
+
.join(delimiter) + '\n');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
toLine(entry, options) {
|
|
69
|
+
if (options.format === 'entries')
|
|
70
|
+
return JSON.stringify(entry) + '\n';
|
|
71
|
+
if (options.format === 'csv') {
|
|
72
|
+
const delimiter = options.delimiter ?? ',';
|
|
73
|
+
const output = (entry.output ?? {});
|
|
74
|
+
return (options.columns
|
|
75
|
+
.map(column => csvEscape(output[columnKey(column)], delimiter))
|
|
76
|
+
.join(delimiter) + '\n');
|
|
77
|
+
}
|
|
78
|
+
const value = typeof entry.output === 'string'
|
|
79
|
+
? entry.output
|
|
80
|
+
: JSON.stringify(entry.output);
|
|
81
|
+
return value + (options.separator ?? '\n');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function join(batch, options) {
|
|
85
|
+
return await History.reconciler(FileJoinActivity, batch, options);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=FileJoinActivity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileJoinActivity.js","sourceRoot":"","sources":["../../src/activities/FileJoinActivity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AsyncActivity, type Binary, type Workflow, WorkflowBatchRef, workflowBatchSchema } from '@t4h.framework/core';
|
|
2
|
+
import { type FileSplitDelimiter } from '../helpers/byte-delimiter-scanner.js';
|
|
3
|
+
export type FileSplitOptions = {
|
|
4
|
+
delimiter: FileSplitDelimiter;
|
|
5
|
+
every?: number;
|
|
6
|
+
skipEmpty?: boolean;
|
|
7
|
+
encoding?: BufferEncoding;
|
|
8
|
+
timeoutInMs?: number;
|
|
9
|
+
};
|
|
10
|
+
export type FileSplitActivityInput = {
|
|
11
|
+
binary: Binary;
|
|
12
|
+
workflow: string;
|
|
13
|
+
options: FileSplitOptions;
|
|
14
|
+
};
|
|
15
|
+
export type FileSplitBatch = {
|
|
16
|
+
batch: string;
|
|
17
|
+
count: number;
|
|
18
|
+
};
|
|
19
|
+
export declare class FileSplitActivity extends AsyncActivity<readonly [
|
|
20
|
+
binary: Binary,
|
|
21
|
+
workflow: Workflow<any, any>,
|
|
22
|
+
options: FileSplitOptions
|
|
23
|
+
], FileSplitActivityInput, FileSplitBatch, WorkflowBatchRef, FileSplitActivityInput, typeof workflowBatchSchema> {
|
|
24
|
+
private readonly claims;
|
|
25
|
+
toInput(binary: Binary, workflow: Workflow<any, any>, options: FileSplitOptions): FileSplitActivityInput;
|
|
26
|
+
bootstrap({ binary, workflow, options, }: FileSplitActivityInput): Promise<import("@t4h.framework/core").AsyncActivityResultPending<FileSplitActivityInput, import("typebox").TObject<{
|
|
27
|
+
batch: import("typebox").TString;
|
|
28
|
+
count: import("typebox").TNumber;
|
|
29
|
+
}>> | import("@t4h.framework/core").AsyncActivityResultFulfilled<FileSplitBatch | undefined>>;
|
|
30
|
+
advance(payload: FileSplitBatch): import("@t4h.framework/core").AsyncActivityResultFulfilled<FileSplitBatch | undefined>;
|
|
31
|
+
toOutput(output: FileSplitBatch): WorkflowBatchRef;
|
|
32
|
+
}
|
|
33
|
+
export declare function split(binary: Binary, workflow: Workflow<any, any>, options: FileSplitOptions): Promise<WorkflowBatchRef>;
|
|
34
|
+
export declare namespace Split {
|
|
35
|
+
const line: (value?: string) => FileSplitDelimiter;
|
|
36
|
+
const char: (value: string, keep?: FileSplitDelimiter["keep"]) => FileSplitDelimiter;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=FileSplitActivity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileSplitActivity.d.ts","sourceRoot":"","sources":["../../src/activities/FileSplitActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,MAAM,EAGX,KAAK,QAAQ,EACb,gBAAgB,EAEhB,mBAAmB,EACpB,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,sCAAsC,CAAA;AAE7C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,kBAAkB,CAAA;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,gBAAgB,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,qBAAa,iBAAkB,SAAQ,aAAa,CAClD,SAAS;IACP,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAC5B,OAAO,EAAE,gBAAgB;CAC1B,EACD,sBAAsB,EACtB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,OAAO,mBAAmB,CAC3B;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGrB;IAEK,OAAO,CACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,EAC5B,OAAO,EAAE,gBAAgB,GACxB,sBAAsB;IASZ,SAAS,CAAC,EACrB,MAAM,EACN,QAAQ,EACR,OAAO,GACR,EAAE,sBAAsB;;;;IAmClB,OAAO,CAAC,OAAO,EAAE,cAAc;IAI/B,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,gBAAgB;CAG1D;AAED,wBAAsB,KAAK,CACzB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,EAC5B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,gBAAgB,CAAC,CAS3B;AAED,yBAAiB,KAAK,CAAC;IACd,MAAM,IAAI,GAAI,cAAY,KAAG,kBAAiC,CAAA;IAE9D,MAAM,IAAI,GACf,OAAO,MAAM,EACb,OAAO,kBAAkB,CAAC,MAAM,CAAC,KAChC,kBAAuC,CAAA;CAC3C"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AsyncActivity, Claim, History, WorkflowBatchRef, WorkflowClaim, workflowBatchSchema, } from '@t4h.framework/core';
|
|
2
|
+
import { FileSystemClaim } from '../FileSystemClaim.js';
|
|
3
|
+
import { ByteDelimiterScanner, } from '../helpers/byte-delimiter-scanner.js';
|
|
4
|
+
export class FileSplitActivity extends AsyncActivity {
|
|
5
|
+
claims = new Claim({
|
|
6
|
+
fs: FileSystemClaim,
|
|
7
|
+
workflow: WorkflowClaim,
|
|
8
|
+
});
|
|
9
|
+
toInput(binary, workflow, options) {
|
|
10
|
+
if (!workflow.id)
|
|
11
|
+
throw new TypeError('Could not split a file into a workflow without an id');
|
|
12
|
+
return { binary, workflow: workflow.id, options };
|
|
13
|
+
}
|
|
14
|
+
async bootstrap({ binary, workflow, options, }) {
|
|
15
|
+
const encoding = options.encoding ?? 'utf8';
|
|
16
|
+
const scanner = new ByteDelimiterScanner({
|
|
17
|
+
delimiter: options.delimiter,
|
|
18
|
+
every: options.every,
|
|
19
|
+
skipEmpty: options.skipEmpty,
|
|
20
|
+
encoding,
|
|
21
|
+
});
|
|
22
|
+
const source = await binary.stream();
|
|
23
|
+
const durable = await this.claims.fs.write(`split/${this.id}`, source.pipe(scanner));
|
|
24
|
+
const slices = await Promise.all(scanner.ranges.map(range => this.claims.fs.slice(durable, range, encoding)));
|
|
25
|
+
const batch = await this.claims.workflow.startMany(workflow, slices);
|
|
26
|
+
if (batch.count === 0)
|
|
27
|
+
return this.done({ batch: batch.id, count: 0 });
|
|
28
|
+
return this.pending({
|
|
29
|
+
schema: workflowBatchSchema,
|
|
30
|
+
...(options.timeoutInMs && {
|
|
31
|
+
timeoutAt: new Date(Date.now() + options.timeoutInMs),
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
advance(payload) {
|
|
36
|
+
return this.done(payload);
|
|
37
|
+
}
|
|
38
|
+
toOutput(output) {
|
|
39
|
+
return new WorkflowBatchRef(output.batch, output.count);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function split(binary, workflow, options) {
|
|
43
|
+
const awaitable = await History.reconciler(FileSplitActivity, binary, workflow, options);
|
|
44
|
+
return await awaitable.wait();
|
|
45
|
+
}
|
|
46
|
+
export var Split;
|
|
47
|
+
(function (Split) {
|
|
48
|
+
Split.line = (value = '\n') => ({ value });
|
|
49
|
+
Split.char = (value, keep) => ({ value, keep });
|
|
50
|
+
})(Split || (Split = {}));
|
|
51
|
+
//# sourceMappingURL=FileSplitActivity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileSplitActivity.js","sourceRoot":"","sources":["../../src/activities/FileSplitActivity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileJoinActivity.spec.d.ts","sourceRoot":"","sources":["../../../src/activities/__tests__/FileJoinActivity.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { buffer } from 'node:stream/consumers';
|
|
3
|
+
import { Claim, WorkflowBatchClaim, WorkflowBatchRef, } from '@t4h.framework/core';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { FileSystemClaim } from '../../FileSystemClaim.js';
|
|
6
|
+
import { FileJoinActivity, join, } from '../FileJoinActivity.js';
|
|
7
|
+
const batchRef = new WorkflowBatchRef('batch-1', 3);
|
|
8
|
+
async function runJoin(entries, options) {
|
|
9
|
+
const joined = { id: 'joined-file' };
|
|
10
|
+
let path;
|
|
11
|
+
let content;
|
|
12
|
+
const write = vi.fn(async (filepath, readable) => {
|
|
13
|
+
path = filepath;
|
|
14
|
+
content = await buffer(readable);
|
|
15
|
+
return joined;
|
|
16
|
+
});
|
|
17
|
+
const outputs = vi.fn(async function* () {
|
|
18
|
+
yield* entries;
|
|
19
|
+
});
|
|
20
|
+
const output = await Claim.run([
|
|
21
|
+
{
|
|
22
|
+
provide: FileSystemClaim,
|
|
23
|
+
value: { write },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
provide: WorkflowBatchClaim,
|
|
27
|
+
value: { outputs },
|
|
28
|
+
},
|
|
29
|
+
], () => {
|
|
30
|
+
const activity = new FileJoinActivity();
|
|
31
|
+
activity.id = 'activity-1';
|
|
32
|
+
return activity.run({ batch: 'batch-1', options });
|
|
33
|
+
});
|
|
34
|
+
return { output, joined, outputs, path, content: content?.toString() };
|
|
35
|
+
}
|
|
36
|
+
const entries = [
|
|
37
|
+
{ index: 0, status: 'fulfilled', output: '10A;x' },
|
|
38
|
+
{ index: 1, status: 'rejected', output: { message: 'boom' } },
|
|
39
|
+
{ index: 2, status: 'fulfilled', output: '10C;z' },
|
|
40
|
+
];
|
|
41
|
+
describe('FileJoinActivity', () => {
|
|
42
|
+
describe('toInput', () => {
|
|
43
|
+
it('stores the batch by id alongside the options', () => {
|
|
44
|
+
const activity = new FileJoinActivity();
|
|
45
|
+
const options = { format: 'lines' };
|
|
46
|
+
expect(activity.toInput(batchRef, options)).toEqual({
|
|
47
|
+
batch: 'batch-1',
|
|
48
|
+
options,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('lines', () => {
|
|
53
|
+
it('concatenates fulfilled outputs by default, one per line', async () => {
|
|
54
|
+
const { output, joined, outputs, content } = await runJoin(entries, {
|
|
55
|
+
format: 'lines',
|
|
56
|
+
});
|
|
57
|
+
expect(outputs).toHaveBeenCalledExactlyOnceWith('batch-1');
|
|
58
|
+
expect(content).toBe('10A;x\n10C;z\n');
|
|
59
|
+
expect(output).toBe(joined);
|
|
60
|
+
});
|
|
61
|
+
it('writes the header and honors a custom separator', async () => {
|
|
62
|
+
const { content } = await runJoin(entries, {
|
|
63
|
+
format: 'lines',
|
|
64
|
+
header: 'id;valor',
|
|
65
|
+
separator: '\r\n',
|
|
66
|
+
});
|
|
67
|
+
expect(content).toBe('id;valor\r\n10A;x\r\n10C;z\r\n');
|
|
68
|
+
});
|
|
69
|
+
it('joins only the statuses selected by the filter', async () => {
|
|
70
|
+
const { content } = await runJoin(entries, {
|
|
71
|
+
format: 'lines',
|
|
72
|
+
status: ['rejected'],
|
|
73
|
+
});
|
|
74
|
+
expect(content).toBe('{"message":"boom"}\n');
|
|
75
|
+
});
|
|
76
|
+
it('names the file after the activity with the format extension', async () => {
|
|
77
|
+
const { path } = await runJoin(entries, { format: 'lines' });
|
|
78
|
+
expect(path).toBe('join/activity-1.txt');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('entries', () => {
|
|
82
|
+
it('dumps every result as NDJSON, rejected included by default', async () => {
|
|
83
|
+
const { content, path } = await runJoin(entries, { format: 'entries' });
|
|
84
|
+
expect(content).toBe('{"index":0,"status":"fulfilled","output":"10A;x"}\n' +
|
|
85
|
+
'{"index":1,"status":"rejected","output":{"message":"boom"}}\n' +
|
|
86
|
+
'{"index":2,"status":"fulfilled","output":"10C;z"}\n');
|
|
87
|
+
expect(path).toBe('join/activity-1.ndjson');
|
|
88
|
+
});
|
|
89
|
+
it('still honors an explicit status filter', async () => {
|
|
90
|
+
const { content } = await runJoin(entries, {
|
|
91
|
+
format: 'entries',
|
|
92
|
+
status: ['fulfilled'],
|
|
93
|
+
});
|
|
94
|
+
expect(content).toBe('{"index":0,"status":"fulfilled","output":"10A;x"}\n' +
|
|
95
|
+
'{"index":2,"status":"fulfilled","output":"10C;z"}\n');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('csv', () => {
|
|
99
|
+
const rows = [
|
|
100
|
+
{ index: 0, status: 'fulfilled', output: { id: 1, name: 'alice' } },
|
|
101
|
+
{
|
|
102
|
+
index: 1,
|
|
103
|
+
status: 'fulfilled',
|
|
104
|
+
output: { id: 2, name: 'quote " comma, line\nbreak' },
|
|
105
|
+
},
|
|
106
|
+
{ index: 2, status: 'fulfilled', output: { id: 3 } },
|
|
107
|
+
{ index: 3, status: 'rejected', output: { message: 'boom' } },
|
|
108
|
+
];
|
|
109
|
+
it('projects the configured columns with a header row', async () => {
|
|
110
|
+
const { content, path } = await runJoin(rows, {
|
|
111
|
+
format: 'csv',
|
|
112
|
+
columns: ['id', 'name'],
|
|
113
|
+
});
|
|
114
|
+
expect(content).toBe('id,name\n' +
|
|
115
|
+
'1,alice\n' +
|
|
116
|
+
'2,"quote "" comma, line\nbreak"\n' +
|
|
117
|
+
'3,\n');
|
|
118
|
+
expect(path).toBe('join/activity-1.csv');
|
|
119
|
+
});
|
|
120
|
+
it('honors a custom delimiter and skips the header when disabled', async () => {
|
|
121
|
+
const { content } = await runJoin(rows.slice(0, 1), {
|
|
122
|
+
format: 'csv',
|
|
123
|
+
columns: ['id', 'name'],
|
|
124
|
+
delimiter: ';',
|
|
125
|
+
header: false,
|
|
126
|
+
});
|
|
127
|
+
expect(content).toBe('1;alice\n');
|
|
128
|
+
});
|
|
129
|
+
it('writes custom header labels for object columns, defaulting to the key', async () => {
|
|
130
|
+
const { content } = await runJoin(rows.slice(0, 1), {
|
|
131
|
+
format: 'csv',
|
|
132
|
+
columns: [{ key: 'id', header: 'Código' }, { key: 'name' }, 'id'],
|
|
133
|
+
});
|
|
134
|
+
expect(content).toBe('Código,name,id\n1,alice,1\n');
|
|
135
|
+
});
|
|
136
|
+
it('escapes header labels containing the delimiter', async () => {
|
|
137
|
+
const { content } = await runJoin([], {
|
|
138
|
+
format: 'csv',
|
|
139
|
+
columns: [{ key: 'total', header: 'Total, em R$' }],
|
|
140
|
+
});
|
|
141
|
+
expect(content).toBe('"Total, em R$"\n');
|
|
142
|
+
});
|
|
143
|
+
it('serializes nested values as JSON inside the cell', async () => {
|
|
144
|
+
const { content } = await runJoin([
|
|
145
|
+
{
|
|
146
|
+
index: 0,
|
|
147
|
+
status: 'fulfilled',
|
|
148
|
+
output: { id: 1, meta: { a: 1 } },
|
|
149
|
+
},
|
|
150
|
+
], { format: 'csv', columns: ['id', 'meta'], header: false });
|
|
151
|
+
expect(content).toBe('1,"{""a"":1}"\n');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe('join', () => {
|
|
156
|
+
it('throws when called outside of an History context', async () => {
|
|
157
|
+
await expect(join(batchRef, { format: 'lines' })).rejects.toThrow('No activities in storage');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
//# sourceMappingURL=FileJoinActivity.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileJoinActivity.spec.js","sourceRoot":"","sources":["../../../src/activities/__tests__/FileJoinActivity.spec.ts"],"names":[],"mappings":"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"}
|