@effected/jsonl 0.1.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/JsonlError.js ADDED
@@ -0,0 +1,246 @@
1
+ import { LineSlice } from "./LineSlice.js";
2
+ import { Option, Schema, SchemaError } from "effect";
3
+
4
+ //#region src/JsonlError.ts
5
+ /**
6
+ * The `@effected/jsonl` error taxonomy.
7
+ *
8
+ * Every tag names a distinct recovery a caller would actually make, and every
9
+ * cause is carried structurally rather than stringified. Core's `PlatformError`
10
+ * passes through untranslated rather than being wrapped.
11
+ *
12
+ * @since 0.1.0
13
+ */
14
+ /**
15
+ * `Schema.SchemaError` as a schema of itself.
16
+ *
17
+ * A schema issue tree is a live object graph, not something with a wire form,
18
+ * so it is declared by its type guard rather than given an encoding. That is
19
+ * what lets {@link InvalidData} be an ordinary schema-backed tagged error while
20
+ * still carrying the failure **structurally** — `error.issue` keeps its paths
21
+ * and expected types — instead of flattening it to a string at the boundary.
22
+ */
23
+ const SchemaErrorFromSelf = Schema.declare(SchemaError.isSchemaError);
24
+ /**
25
+ * A journal line that is not valid JSON.
26
+ *
27
+ * **This is the expected steady state at the tail of a live journal**, not
28
+ * necessarily corruption: a writer caught mid-`write` leaves a partial final
29
+ * line, and `LineSlice.terminated` is what distinguishes the two cases. An
30
+ * unterminated malformed line is a torn tail that the next append completes; a
31
+ * *terminated* malformed line is a hole in the history that will never heal.
32
+ *
33
+ * Malformed input always fails through this typed channel — never as a defect,
34
+ * and never by being silently dropped from a read.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { Line } from "@effected/jsonl";
39
+ * import { Result } from "effect";
40
+ *
41
+ * const [line] = Line.split('{"a":');
42
+ * const parsed = Line.parseResult(line!);
43
+ * if (Result.isFailure(parsed)) {
44
+ * parsed.failure.line.terminated; // false — a torn tail, not a hole
45
+ * }
46
+ * ```
47
+ *
48
+ * @public
49
+ */
50
+ var MalformedLine = class extends Schema.TaggedErrorClass()("MalformedLine", {
51
+ /** The offending line, with its byte offsets into the source. */
52
+ line: LineSlice }) {
53
+ get message() {
54
+ return `JSONL ${this.line.terminated ? "malformed line" : "unterminated final line"} at byte offset ${this.line.offset}`;
55
+ }
56
+ };
57
+ /**
58
+ * A line whose `event` tag is not in the registry.
59
+ *
60
+ * Typed rather than a defect on purpose: a journal written by an older or newer
61
+ * version of the same application is hostile input in the technical sense, and
62
+ * a reader that crashed on an unrecognized tag would be unable to skip forward
63
+ * past one. The known tags travel with the error so a caller can report the
64
+ * mismatch without reaching back for the registry.
65
+ *
66
+ * @public
67
+ */
68
+ var UnknownEvent = class extends Schema.TaggedErrorClass()("UnknownEvent", {
69
+ /** The offending line, with its byte offsets into the source. */
70
+ line: LineSlice,
71
+ /** The unrecognized tag as it appeared on the envelope. */
72
+ event: Schema.String,
73
+ /** The tags this journal's registry does define. */
74
+ known: Schema.Array(Schema.String)
75
+ }) {
76
+ get message() {
77
+ return `unknown JSONL event ${JSON.stringify(this.event)} at byte offset ${this.line.offset}`;
78
+ }
79
+ };
80
+ /**
81
+ * A line whose envelope or payload failed schema validation.
82
+ *
83
+ * Covers both stages of the two-stage decode, distinguished by `event`: the
84
+ * frame itself (`Option.none()` — the line is JSON but not an envelope) and a
85
+ * registered payload (`Option.some(tag)` — the envelope is well-formed but its
86
+ * `data` does not match the schema registered for that tag).
87
+ *
88
+ * The `SchemaError` is carried **whole**, so `error.issue` is the full issue
89
+ * tree with its paths and expected types intact. Nothing here is stringified;
90
+ * `message` renders lazily and only when something asks for it.
91
+ *
92
+ * @public
93
+ */
94
+ var InvalidData = class extends Schema.TaggedErrorClass()("InvalidData", {
95
+ /** The offending line, with its byte offsets into the source. */
96
+ line: LineSlice,
97
+ /**
98
+ * The event tag whose payload schema rejected the data, or `none` when it
99
+ * was the envelope frame itself that failed.
100
+ */
101
+ event: Schema.Option(Schema.String),
102
+ /** The schema failure, carried structurally — `issue` is the full tree. */
103
+ error: SchemaErrorFromSelf
104
+ }) {
105
+ get message() {
106
+ return `invalid JSONL ${Option.isSome(this.event) ? `payload for event ${JSON.stringify(this.event.value)}` : "envelope"} at byte offset ${this.line.offset}: ${this.error.message}`;
107
+ }
108
+ };
109
+ /**
110
+ * An append attempted after a terminal event, by an event not marked `reopen`.
111
+ *
112
+ * A journal whose tail is terminal is quiescent: it is finished, and appending
113
+ * to it would silently resurrect a closed loop. Reopening is legal but must be
114
+ * declared, which is what `reopen` marks.
115
+ *
116
+ * @public
117
+ */
118
+ var TerminalViolation = class extends Schema.TaggedErrorClass()("TerminalViolation", {
119
+ /** The tag of the event whose append was refused. */
120
+ event: Schema.String,
121
+ /** The terminal event currently at the tail of the journal. */
122
+ terminal: Schema.String
123
+ }) {
124
+ get message() {
125
+ return `cannot append ${JSON.stringify(this.event)}: the journal is terminal at ${JSON.stringify(this.terminal)}`;
126
+ }
127
+ };
128
+ /**
129
+ * An operation against a journal file that does not exist.
130
+ *
131
+ * A missing journal is a **legal state** — building the layer over a path that
132
+ * does not exist yet succeeds, and the watcher activates once the file appears.
133
+ * What is not legal is materializing it implicitly: `append`, `query` and
134
+ * `latest` fail with this rather than creating the file, so a typo in a path
135
+ * cannot quietly produce a second, empty journal that looks like a working
136
+ * system with no history. Creation is always explicit, via `create`.
137
+ *
138
+ * @public
139
+ */
140
+ var JournalNotFound = class extends Schema.TaggedErrorClass()("JournalNotFound", {
141
+ /** The path that does not exist. */
142
+ path: Schema.String }) {
143
+ get message() {
144
+ return `journal not found: ${this.path}`;
145
+ }
146
+ };
147
+ /**
148
+ * A payload that validated against its schema but cannot be serialized to JSON.
149
+ *
150
+ * The two reachable causes are a `bigint` anywhere in the encoded value and a
151
+ * reference cycle; `JSON.stringify` throws a `TypeError` for both. A payload
152
+ * schema is free to permit either — `Schema.Unknown` permits everything — so
153
+ * schema validity does not imply serializability and the encode path has to
154
+ * treat them as separate questions.
155
+ *
156
+ * This is its own tag rather than a flavour of {@link InvalidData} because the
157
+ * recovery differs: `InvalidData` means the value is wrong and the caller
158
+ * should fix the value, while this means the value is fine but unrepresentable
159
+ * in this format and the caller must change the *shape* they are journaling.
160
+ *
161
+ * The thrown value is carried as-is under `cause` — never stringified, and
162
+ * never re-thrown. A raw `TypeError` escaping the encode path would be
163
+ * especially bad here: `Effect.fromResult` evaluates its argument eagerly, so
164
+ * an unguarded throw happens at **construction** of the `Effect`, before any
165
+ * fiber exists, where not even `Effect.runSyncExit` can catch it.
166
+ *
167
+ * @public
168
+ */
169
+ var UnserializableData = class extends Schema.TaggedErrorClass()("UnserializableData", {
170
+ /** The event tag whose payload could not be serialized. */
171
+ event: Schema.String,
172
+ /** The value `JSON.stringify` threw, carried structurally. */
173
+ cause: Schema.Unknown
174
+ }) {
175
+ get message() {
176
+ const detail = this.cause instanceof Error ? this.cause.message : "value is not JSON-serializable";
177
+ return `cannot serialize payload for event ${JSON.stringify(this.event)}: ${detail}`;
178
+ }
179
+ };
180
+ /**
181
+ * An append refused because the journal's scope has closed.
182
+ *
183
+ * Its own tag rather than a flavour of {@link TerminalViolation}, because the
184
+ * recoveries have nothing in common: a terminal journal is a *state* the
185
+ * consumer can reason about and reopen from with a `reopen` event, while a
186
+ * closed one is a *lifecycle* fact — this service is gone, and the only moves
187
+ * are to build a new layer or stop.
188
+ *
189
+ * Refusal is deliberately a typed failure rather than a wait. A `Latch` would
190
+ * suspend the late append with no failure channel, turning "the journal is
191
+ * closing" into a hang; failing fast is what lets a caller react.
192
+ *
193
+ * @public
194
+ */
195
+ var JournalClosed = class extends Schema.TaggedErrorClass()("JournalClosed", {
196
+ /** The tag of the event whose append was refused. */
197
+ event: Schema.String }) {
198
+ get message() {
199
+ return `cannot append ${JSON.stringify(this.event)}: the journal is closed`;
200
+ }
201
+ };
202
+ /**
203
+ * The journal file was truncated or replaced beneath a reader.
204
+ *
205
+ * The cooperative-writer contract is append-only: a journal only ever grows,
206
+ * and every cursor this package hands out depends on that. When the file shrinks
207
+ * below a tracked offset, or the path comes to name a different file entirely,
208
+ * the contract has been broken by something outside the package and every
209
+ * offset-derived belief is now meaningless.
210
+ *
211
+ * Surfaced rather than repaired, deliberately. Silently re-reading from zero
212
+ * would paper over a real operational fault — a rotating log shipper, a
213
+ * `>` where `>>` was meant — and leave projections quietly inconsistent with
214
+ * the file. The recovery is the consumer's: discard cursor-derived state,
215
+ * re-read, and tell somebody.
216
+ *
217
+ * One tag rather than two, though `reason` distinguishes the causes: truncation
218
+ * and replacement have the **same** recovery, and a tag per cause would split
219
+ * one recovery across two tags.
220
+ *
221
+ * @remarks
222
+ * Detection is as complete as the platform allows and no more. Truncation is
223
+ * caught by size; replacement is caught by inode identity, which
224
+ * `FileSystem.File.Info` exposes as an **`Option`** — on a platform that does
225
+ * not report it, a replacement at equal or greater size is undetectable and
226
+ * only truncation is caught.
227
+ *
228
+ * @public
229
+ */
230
+ var JournalResync = class extends Schema.TaggedErrorClass()("JournalResync", {
231
+ /** The journal path whose file changed identity or shrank. */
232
+ path: Schema.String,
233
+ /** Which contract breach was detected. Diagnostic; the recovery is the same. */
234
+ reason: Schema.Literals(["truncated", "replaced"]),
235
+ /** The logical offset the reader had consumed to. */
236
+ expected: Schema.Number,
237
+ /** The file's logical size when the breach was noticed. */
238
+ actual: Schema.Number
239
+ }) {
240
+ get message() {
241
+ return `journal ${this.reason} beneath the reader at ${this.path}: consumed ${this.expected}, file is now ${this.actual}`;
242
+ }
243
+ };
244
+
245
+ //#endregion
246
+ export { InvalidData, JournalClosed, JournalNotFound, JournalResync, MalformedLine, TerminalViolation, UnknownEvent, UnserializableData };
package/JsonlEvent.js ADDED
@@ -0,0 +1,45 @@
1
+ //#region src/JsonlEvent.ts
2
+ /**
3
+ * Runtime type identifier marking a JSONL event definition.
4
+ *
5
+ * @public
6
+ */
7
+ const JsonlEventTypeId = "~effected/jsonl/JsonlEvent";
8
+ /**
9
+ * Defines an event.
10
+ *
11
+ * The `const` type parameters are load-bearing: they keep `"unlinked"` a
12
+ * literal rather than widening it to `string`, and keep `terminal: true` a
13
+ * literal `true`, so both survive into the derived envelope union where the
14
+ * narrowing and the terminal/reopen state machine depend on them.
15
+ *
16
+ * `data` is required rather than defaulted. A payload-less event says so
17
+ * explicitly with `Schema.Void`, because a silent default would make a typo in
18
+ * the options object look like a deliberate void payload.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { JsonlEvent } from "@effected/jsonl";
23
+ * import { Schema } from "effect";
24
+ *
25
+ * const MailReceived = JsonlEvent.make("mail-received", {
26
+ * data: Schema.Struct({ round: Schema.Number }),
27
+ * });
28
+ * const Unlinked = JsonlEvent.make("unlinked", { data: Schema.Void, terminal: true });
29
+ * const Relinked = JsonlEvent.make("relinked", { data: Schema.Void, reopen: true });
30
+ *
31
+ * const registry = [MailReceived, Unlinked, Relinked] as const;
32
+ * ```
33
+ *
34
+ * @public
35
+ */
36
+ const JsonlEvent = { make: (tag, options) => ({
37
+ [JsonlEventTypeId]: JsonlEventTypeId,
38
+ tag,
39
+ data: options.data,
40
+ terminal: options.terminal ?? false,
41
+ reopen: options.reopen ?? false
42
+ }) };
43
+
44
+ //#endregion
45
+ export { JsonlEvent, JsonlEventTypeId };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
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/Line.js ADDED
@@ -0,0 +1,219 @@
1
+ import { LineSlice } from "./LineSlice.js";
2
+ import { MalformedLine } from "./JsonlError.js";
3
+ import { utf8Length } from "./internal/utf8.js";
4
+ import { Option, Result, Schema } from "effect";
5
+
6
+ //#region src/Line.ts
7
+ /**
8
+ * The pure, synchronous line layer of a JSONL journal.
9
+ *
10
+ * Nothing here touches `FileSystem`, builds an `Effect`, or needs a runtime —
11
+ * a `PreToolUse` hook script reads the current state of a journal with one
12
+ * call. That is a contract, not a convenience: a hook that had to construct an
13
+ * Effect runtime to read one line would not adopt this package.
14
+ *
15
+ * @since 0.1.0
16
+ */
17
+ /**
18
+ * A line that parsed as JSON, paired with the slice it came from.
19
+ *
20
+ * `value` is deliberately `unknown`: this layer knows JSON, not envelopes.
21
+ * Validating `event`, `at`, `scope` and the registered payload schema is the
22
+ * envelope layer's job, and keeping the split means a malformed *envelope* and
23
+ * a malformed *line* stay distinguishable failures.
24
+ *
25
+ * @public
26
+ */
27
+ var ParsedLine = class extends Schema.Class("ParsedLine")({
28
+ /** Where this line lives in the source. */
29
+ line: LineSlice,
30
+ /** The parsed JSON value — any JSON value, not necessarily an object. */
31
+ value: Schema.Unknown
32
+ }) {};
33
+ /** Whether a line carries nothing but whitespace. */
34
+ const isBlank = (line) => line.text.trim() === "";
35
+ /**
36
+ * Splitting, parsing and corrupt-tail walk-back over JSONL text.
37
+ *
38
+ * Every operation is total and synchronous: it returns a value for every
39
+ * input, including empty text, torn tails, and text that is not JSONL at all.
40
+ * Nothing here throws — a `JSON.parse` failure becomes a {@link MalformedLine}
41
+ * in the returned `Result`, because a journal is untrusted input and untrusted
42
+ * input fails typed.
43
+ *
44
+ * @public
45
+ */
46
+ var Line = class Line {
47
+ /**
48
+ * The UTF-8 byte length of a string.
49
+ *
50
+ * Exposed because callers doing their own offset arithmetic must measure
51
+ * the same way this module does. `String.length` is a different number for
52
+ * any non-ASCII line.
53
+ *
54
+ * An unpaired surrogate counts as the 3 bytes of U+FFFD, matching what a
55
+ * UTF-8 write actually emits. The offsets therefore stay true to the file
56
+ * even though the character itself cannot survive the round trip — text
57
+ * read back from a UTF-8 journal never contains one.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { Line } from "@effected/jsonl";
62
+ *
63
+ * Line.byteLength("\u{1F600}"); // 4
64
+ * "\u{1F600}".length; // 2 — the trap
65
+ * ```
66
+ */
67
+ static byteLength(text) {
68
+ return utf8Length(text);
69
+ }
70
+ /**
71
+ * Split text into candidate lines with byte-exact offsets.
72
+ *
73
+ * Lines are separated by `\n`; a `\r` immediately preceding it is treated as
74
+ * part of the terminator, so CRLF journals split identically to LF ones. A
75
+ * bare `\r` is ordinary content, because only `\n` terminates.
76
+ *
77
+ * A trailing terminator does **not** produce a phantom empty final line, so
78
+ * a well-formed journal yields exactly as many slices as it has entries.
79
+ * Interior blank lines *are* returned — they are real bytes at real offsets,
80
+ * and hiding them would make the offsets lie.
81
+ *
82
+ * Only the final slice can have `terminated: false`.
83
+ *
84
+ * @param text - JSONL source text.
85
+ * @returns One {@link LineSlice} per candidate line, in source order.
86
+ */
87
+ static split(text) {
88
+ const lines = [];
89
+ let offset = 0;
90
+ let start = 0;
91
+ for (;;) {
92
+ const newline = text.indexOf("\n", start);
93
+ if (newline === -1) break;
94
+ const raw = text.slice(start, newline);
95
+ const hadCarriageReturn = raw.endsWith("\r");
96
+ const content = hadCarriageReturn ? raw.slice(0, -1) : raw;
97
+ const length = utf8Length(content);
98
+ const end = offset + length + (hadCarriageReturn ? 2 : 1);
99
+ lines.push(new LineSlice({
100
+ offset,
101
+ end,
102
+ length,
103
+ text: content,
104
+ terminated: true
105
+ }));
106
+ offset = end;
107
+ start = newline + 1;
108
+ }
109
+ if (start < text.length) {
110
+ const content = text.slice(start);
111
+ const length = utf8Length(content);
112
+ lines.push(new LineSlice({
113
+ offset,
114
+ end: offset + length,
115
+ length,
116
+ text: content,
117
+ terminated: false
118
+ }));
119
+ }
120
+ return lines;
121
+ }
122
+ /**
123
+ * The byte offset up to which this text has been fully consumed.
124
+ *
125
+ * This is the offset past the last **terminated** line. An unterminated
126
+ * final line is left unconsumed on purpose: it may be a writer caught
127
+ * mid-append, and re-reading from this offset once the file grows sees the
128
+ * completed line rather than gluing a stale fragment to fresh bytes.
129
+ *
130
+ * @param text - JSONL source text.
131
+ * @returns The resume cursor, in UTF-8 bytes.
132
+ */
133
+ static consumedOffset(text) {
134
+ const last = Line.split(text).at(-1);
135
+ if (last === void 0) return 0;
136
+ return last.terminated ? last.end : last.offset;
137
+ }
138
+ /**
139
+ * Parse one candidate line's JSON.
140
+ *
141
+ * Any JSON value succeeds — objects, arrays and scalars alike. This layer
142
+ * does not know what an envelope is.
143
+ *
144
+ * @param line - A slice from {@link Line.split}.
145
+ * @returns The {@link ParsedLine}, or a {@link MalformedLine} carrying the
146
+ * slice so the caller can locate the damage and decide whether an
147
+ * unterminated line is a torn tail worth waiting for.
148
+ */
149
+ static parseResult(line) {
150
+ let value;
151
+ try {
152
+ value = JSON.parse(line.text);
153
+ } catch {
154
+ return Result.fail(new MalformedLine({ line }));
155
+ }
156
+ return Result.succeed(new ParsedLine({
157
+ line,
158
+ value
159
+ }));
160
+ }
161
+ /**
162
+ * Parse every non-blank line, reporting failures rather than dropping them.
163
+ *
164
+ * There is deliberately no "valid lines only" variant. A hole in the middle
165
+ * of a journal is information — it means a line was written that no reader
166
+ * can interpret — and whether that is tolerable is the caller's decision,
167
+ * not this module's. Filter the `Result`s yourself and the choice stays
168
+ * visible at the call site.
169
+ *
170
+ * Whitespace-only lines are skipped rather than reported: they carry no
171
+ * information to lose, and reporting them would make a hand-edited journal
172
+ * look corrupt.
173
+ *
174
+ * @param text - JSONL source text.
175
+ * @returns One `Result` per non-blank line, in source order.
176
+ */
177
+ static parseAll(text) {
178
+ const results = [];
179
+ for (const line of Line.split(text)) if (!isBlank(line)) results.push(Line.parseResult(line));
180
+ return results;
181
+ }
182
+ /**
183
+ * Walk back from the end to the last line that parses.
184
+ *
185
+ * This is the whole read path for a snapshot-style journal, where the
186
+ * current state *is* the last valid line: a session killed mid-append leaves
187
+ * a partial final line, and the walk-back steps over it — and over any
188
+ * number of malformed or blank trailing lines — to the last line that means
189
+ * something.
190
+ *
191
+ * Parsing stops at the first success, so the cost is proportional to the
192
+ * damage at the tail rather than to the age of the file.
193
+ *
194
+ * **A torn tail is only detectable when its fragment fails to parse.**
195
+ * Every strict prefix of a JSON *object* is invalid JSON, so a truncated
196
+ * `{"event":"unlinked"}` is always caught. A truncated *scalar* is not:
197
+ * `42` cut mid-write leaves `4`, which parses cleanly as a different value.
198
+ * The JSON layer cannot close that hole — the envelope layer can, because
199
+ * `4` is not an envelope, which is one more reason the envelope is the
200
+ * package's contract rather than an option. Check `LineSlice.terminated` on
201
+ * the result when it matters.
202
+ *
203
+ * @param text - JSONL source text.
204
+ * @returns The last parseable line, or `Option.none()` if none parses.
205
+ */
206
+ static lastValid(text) {
207
+ const lines = Line.split(text);
208
+ for (let index = lines.length - 1; index >= 0; index--) {
209
+ const line = lines[index];
210
+ if (line === void 0 || isBlank(line)) continue;
211
+ const parsed = Line.parseResult(line);
212
+ if (Result.isSuccess(parsed)) return Option.some(parsed.success);
213
+ }
214
+ return Option.none();
215
+ }
216
+ };
217
+
218
+ //#endregion
219
+ export { Line, ParsedLine };
package/LineSlice.js ADDED
@@ -0,0 +1,62 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/LineSlice.ts
4
+ /**
5
+ * One candidate line of a JSONL journal, located in the source by byte.
6
+ *
7
+ * @since 0.1.0
8
+ */
9
+ /**
10
+ * A single candidate line: its text, and where it lives in the source **in
11
+ * bytes**.
12
+ *
13
+ * Every offset on this class is a UTF-8 byte offset, never a UTF-16 code-unit
14
+ * index, because these values are cursors into a file: they are handed to
15
+ * `FileSystem.stream`'s `offset` option and persisted across process restarts.
16
+ * A `String.length`-derived offset is correct only for ASCII journals and is
17
+ * the single most likely bug in this module.
18
+ *
19
+ * The terminator is **not** part of the content: `text` and `length` exclude
20
+ * the trailing `\n`, and exclude the `\r` of a `\r\n` pair. `end` includes it,
21
+ * which is why `end - offset` is not always `length`.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { Line } from "@effected/jsonl";
26
+ *
27
+ * const [first] = Line.split('{"a":1}\r\n{"b":2}\n');
28
+ * first?.text; // '{"a":1}'
29
+ * first?.offset; // 0
30
+ * first?.length; // 7 — content bytes, no terminator
31
+ * first?.end; // 9 — past the CR and the LF
32
+ * first?.terminated; // true
33
+ * ```
34
+ *
35
+ * @public
36
+ */
37
+ var LineSlice = class extends Schema.Class("LineSlice")({
38
+ /** UTF-8 byte offset of this line's first content byte. */
39
+ offset: Schema.Number,
40
+ /**
41
+ * UTF-8 byte offset just past this line's terminator — the offset at which
42
+ * the next line begins, and the resume cursor for an incremental read.
43
+ *
44
+ * Equal to `offset + length` when the line is unterminated.
45
+ */
46
+ end: Schema.Number,
47
+ /** UTF-8 byte length of `LineSlice.text`, excluding any terminator. */
48
+ length: Schema.Number,
49
+ /** The line's content, with its terminator and any paired `\r` removed. */
50
+ text: Schema.String,
51
+ /**
52
+ * Whether a `\n` terminated this line in the source.
53
+ *
54
+ * `false` can only occur on the final line, and means the line **may be a
55
+ * torn tail** — a writer caught mid-append. A reader walks back over it and
56
+ * leaves its bytes unconsumed so the next read sees the completed line.
57
+ */
58
+ terminated: Schema.Boolean
59
+ }) {};
60
+
61
+ //#endregion
62
+ export { LineSlice };