-
-
Notifications
You must be signed in to change notification settings - Fork 693
Expand file tree
/
Copy path30_encoding.ts
More file actions
163 lines (143 loc) · 6.58 KB
/
Copy path30_encoding.ts
File metadata and controls
163 lines (143 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* @title Decoding and encoding streams
*
* Use `Stream.pipeThroughChannel` with the `Ndjson` and `SchemaBinary` modules to
* decode and encode streams of structured data.
*/
import { DateTime, Schema, Stream } from "effect"
import { Ndjson, SchemaBinary } from "effect/unstable/encoding"
// SchemaBinary derives a framed binary decoder directly from a schema.
export const schemaBinaryDecoder = SchemaBinary.decode(Schema.Struct({
id: Schema.Int,
name: Schema.String
}))()
// ---------------------------------------------------------------------------
// Domain
// ---------------------------------------------------------------------------
// A log entry schema representing structured log events. In practice these
// would come from a file, HTTP body, or socket connection.
// `DateTimeUtcFromString` decodes an ISO-8601 string into a `DateTime.Utc`.
class LogEntry extends Schema.Class<LogEntry>("LogEntry")({
timestamp: Schema.DateTimeUtcFromString,
level: Schema.Literals(["info", "warn", "error"]),
message: Schema.String
}) {}
// ---------------------------------------------------------------------------
// Decoding NDJSON strings → objects
// ---------------------------------------------------------------------------
// Suppose we receive raw NDJSON text from a file or network socket.
// `Ndjson.decodeString()` is a Channel that splits incoming strings on
// newlines and `JSON.parse`s each line.
// Pipe the stream through the channel with `Stream.pipeThroughChannel`.
export const decodeUntyped = Stream.make(
"{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"start\"}\n" +
"{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"oops\"}\n"
).pipe(
Stream.pipeThroughChannel(Ndjson.decodeString()),
Stream.runCollect
)
// When you need schema validation on top of the raw JSON parse, use
// `Ndjson.decodeSchemaString(Schema)()`. This decodes each line, parses the
// JSON, and then validates each value against the schema — all in one channel.
export const decodeTyped = Stream.make(
"{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"start\"}\n" +
"{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"oops\"}\n"
).pipe(
Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()),
Stream.runCollect
)
// ---------------------------------------------------------------------------
// Encoding objects → NDJSON strings
// ---------------------------------------------------------------------------
// `Ndjson.encodeString()` serialises each value to a JSON line.
// The resulting stream emits ready-to-write NDJSON strings.
export const encodeUntyped = Stream.make(
{ timestamp: "2025-06-01T00:00:00Z", level: "info", message: "start" },
{ timestamp: "2025-06-01T00:00:01Z", level: "error", message: "oops" }
).pipe(
Stream.pipeThroughChannel(Ndjson.encodeString()),
Stream.runCollect
)
// `Ndjson.encodeSchemaString(Schema)()` encodes each value through the schema
// first (applying any transformations such as date formatting), then
// serialises it to an NDJSON line.
export const encodeTyped = Stream.make(
new LogEntry({
timestamp: DateTime.makeUnsafe("2025-06-01T00:00:00Z"),
level: "info",
message: "start"
}),
new LogEntry({
timestamp: DateTime.makeUnsafe("2025-06-01T00:00:01Z"),
level: "error",
message: "oops"
})
).pipe(
Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()),
Stream.runCollect
)
// ---------------------------------------------------------------------------
// Binary (Uint8Array) variants
// ---------------------------------------------------------------------------
// When working with binary I/O (e.g. TCP sockets, file descriptors) use the
// non-string variants. `Ndjson.decode()` expects `Uint8Array` chunks and
// handles text decoding internally. `Ndjson.encode()` produces `Uint8Array`
// output.
const enc = new TextEncoder()
export const decodeBinary = Stream.make(
enc.encode("{\"level\":\"info\",\"message\":\"binary\"}\n")
).pipe(
Stream.pipeThroughChannel(Ndjson.decode()),
Stream.runCollect
)
export const encodeBinary = Stream.make(
{ level: "info", message: "binary" }
).pipe(
Stream.pipeThroughChannel(Ndjson.encode()),
Stream.runCollect
)
// ---------------------------------------------------------------------------
// Handling empty lines
// ---------------------------------------------------------------------------
// NDJSON files sometimes contain blank lines (e.g. trailing newlines or
// pretty-printed output). Pass `{ ignoreEmptyLines: true }` to skip them
// instead of raising an `NdjsonError`.
export const decodeIgnoringBlanks = Stream.make(
"{\"ok\":true}\n\n{\"ok\":false}\n"
).pipe(
Stream.pipeThroughChannel(Ndjson.decodeString({ ignoreEmptyLines: true })),
Stream.runCollect
)
// ---------------------------------------------------------------------------
// Error handling
// ---------------------------------------------------------------------------
// `Ndjson.NdjsonError` is raised when encoding (`kind: "Pack"`) or decoding
// (`kind: "Unpack"`) fails. You can catch it with `Stream.catchTag` or
// `Effect.catchTag`.
export const handleDecodeErrors = Stream.make("not-valid-json\n").pipe(
Stream.pipeThroughChannel(Ndjson.decodeString()),
Stream.catchTag("NdjsonError", (err) =>
// The `kind` field indicates whether the error occurred during
// encoding ("Pack") or decoding ("Unpack"), and `cause` contains
// the underlying exception.
Stream.succeed({ recovered: true, kind: err.kind })),
Stream.runCollect
)
// ---------------------------------------------------------------------------
// Realistic pipeline: decode → transform → re-encode
// ---------------------------------------------------------------------------
// A common pattern is to read NDJSON, transform each record, and write it
// back as NDJSON. This example filters error-level log entries and re-encodes
// them.
const ndjsonInput = "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"ok\"}\n" +
"{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"fail\"}\n" +
"{\"timestamp\":\"2025-06-01T00:00:02Z\",\"level\":\"warn\",\"message\":\"slow\"}\n"
export const filterAndReencode = Stream.make(ndjsonInput).pipe(
// Decode each line into a validated LogEntry
Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()),
// Keep only error-level entries
Stream.filter((entry) => entry.level === "error"),
// Re-encode the filtered entries back to NDJSON strings
Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()),
Stream.runCollect
)