@effected/jsonl 0.9.0 → 0.11.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/index.d.ts CHANGED
@@ -1,62 +1,14 @@
1
- import { Context, DateTime, Duration, Effect, FileSystem, Layer, Option, PlatformError, PubSub, Result, Schema, Stream, SubscriptionRef, Take } from "effect";
2
- //#region src/LineSlice.d.ts
3
- declare const LineSlice_base: Schema.Class<LineSlice, Schema.Struct<{
4
- /** UTF-8 byte offset of this line's first content byte. */
5
- readonly offset: Schema.Number;
6
- /**
7
- * UTF-8 byte offset just past this line's terminator — the offset at which
8
- * the next line begins, and the resume cursor for an incremental read.
9
- *
10
- * Equal to `offset + length` when the line is unterminated.
11
- */
12
- readonly end: Schema.Number;
13
- /** UTF-8 byte length of `LineSlice.text`, excluding any terminator. */
14
- readonly length: Schema.Number;
15
- /** The line's content, with its terminator and any paired `\r` removed. */
16
- readonly text: Schema.String;
17
- /**
18
- * Whether a `\n` terminated this line in the source.
19
- *
20
- * `false` can only occur on the final line, and means the line **may be a
21
- * torn tail** — a writer caught mid-append. A reader walks back over it and
22
- * leaves its bytes unconsumed so the next read sees the completed line.
23
- */
24
- readonly terminated: Schema.Boolean;
25
- }>, {}>;
26
- /**
27
- * A single candidate line: its text, and where it lives in the source **in
28
- * bytes**.
29
- *
30
- * Every offset on this class is a UTF-8 byte offset, never a UTF-16 code-unit
31
- * index, because these values are cursors into a file: they are handed to
32
- * `FileSystem.stream`'s `offset` option and persisted across process restarts.
33
- * A `String.length`-derived offset is correct only for ASCII journals and is
34
- * the single most likely bug in this module.
35
- *
36
- * The terminator is **not** part of the content: `text` and `length` exclude
37
- * the trailing `\n`, and exclude the `\r` of a `\r\n` pair. `end` includes it,
38
- * which is why `end - offset` is not always `length`.
39
- *
40
- * @example
41
- * ```ts
42
- * import { Line } from "@effected/jsonl";
43
- *
44
- * const [first] = Line.split('{"a":1}\r\n{"b":2}\n');
45
- * first?.text; // '{"a":1}'
46
- * first?.offset; // 0
47
- * first?.length; // 7 — content bytes, no terminator
48
- * first?.end; // 9 — past the CR and the LF
49
- * first?.terminated; // true
50
- * ```
51
- *
52
- * @public
53
- */
54
- export declare class LineSlice extends LineSlice_base {}
55
- //#endregion
1
+ import { Context, DateTime, Duration, Effect, FileSystem, Layer, Option, PlatformError, Result, Schema, Scope, Stream } from "effect";
56
2
  //#region src/JsonlError.d.ts
57
3
  declare const MalformedLine_base: Schema.Class<MalformedLine, Schema.TaggedStruct<"MalformedLine", {
58
4
  /** The offending line, with its byte offsets into the source. */
59
- readonly line: typeof LineSlice;
5
+ readonly line: Schema.Struct<{
6
+ readonly offset: Schema.Number;
7
+ readonly end: Schema.Number;
8
+ readonly length: Schema.Number;
9
+ readonly text: Schema.String;
10
+ readonly terminated: Schema.Boolean;
11
+ }>;
60
12
  }>, import("effect/Cause").YieldableError>;
61
13
  /**
62
14
  * A journal line that is not valid JSON.
@@ -88,8 +40,14 @@ export declare class MalformedLine extends MalformedLine_base {
88
40
  get message(): string;
89
41
  }
90
42
  declare const UnknownEvent_base: Schema.Class<UnknownEvent, Schema.TaggedStruct<"UnknownEvent", {
91
- /** The offending line, with its byte offsets into the source. */
92
- readonly line: typeof LineSlice;
43
+ /** The offending line, with its byte offsets into the source. Absent on the encode path. */
44
+ readonly line: Schema.optionalKey<Schema.Struct<{
45
+ readonly offset: Schema.Number;
46
+ readonly end: Schema.Number;
47
+ readonly length: Schema.Number;
48
+ readonly text: Schema.String;
49
+ readonly terminated: Schema.Boolean;
50
+ }>>;
93
51
  /** The unrecognized tag as it appeared on the envelope. */
94
52
  readonly event: Schema.String;
95
53
  /** The tags this journal's registry does define. */
@@ -110,8 +68,17 @@ export declare class UnknownEvent extends UnknownEvent_base {
110
68
  get message(): string;
111
69
  }
112
70
  declare const InvalidData_base: Schema.Class<InvalidData, Schema.TaggedStruct<"InvalidData", {
113
- /** The offending line, with its byte offsets into the source. */
114
- readonly line: typeof LineSlice;
71
+ /**
72
+ * The offending line, with its byte offsets into the source. Absent when the
73
+ * failure came from encoding a payload for append, which has no line yet.
74
+ */
75
+ readonly line: Schema.optionalKey<Schema.Struct<{
76
+ readonly offset: Schema.Number;
77
+ readonly end: Schema.Number;
78
+ readonly length: Schema.Number;
79
+ readonly text: Schema.String;
80
+ readonly terminated: Schema.Boolean;
81
+ }>>;
115
82
  /**
116
83
  * The event tag whose payload schema rejected the data, or `none` when it
117
84
  * was the envelope frame itself that failed.
@@ -164,8 +131,8 @@ declare const JournalNotFound_base: Schema.Class<JournalNotFound, Schema.TaggedS
164
131
  *
165
132
  * A missing journal is a **legal state** — building the layer over a path that
166
133
  * does not exist yet succeeds, and the watcher activates once the file appears.
167
- * What is not legal is materializing it implicitly: `append`, `query` and
168
- * `latest` fail with this rather than creating the file, so a typo in a path
134
+ * What is not legal is materializing it implicitly: `append` and `query` fail
135
+ * with this (and `latest` reads as empty) rather than creating the file, so a typo in a path
169
136
  * cannot quietly produce a second, empty journal that looks like a working
170
137
  * system with no history. Creation is always explicit, via `create`.
171
138
  *
@@ -178,7 +145,7 @@ declare const UnserializableData_base: Schema.Class<UnserializableData, Schema.T
178
145
  /** The event tag whose payload could not be serialized. */
179
146
  readonly event: Schema.String;
180
147
  /** The value `JSON.stringify` threw, carried structurally. */
181
- readonly cause: Schema.Unknown;
148
+ readonly cause: Schema.Defect;
182
149
  }>, import("effect/Cause").YieldableError>;
183
150
  /**
184
151
  * A payload that validated against its schema but cannot be serialized to JSON.
@@ -268,6 +235,13 @@ declare const JournalResync_base: Schema.Class<JournalResync, Schema.TaggedStruc
268
235
  export declare class JournalResync extends JournalResync_base {
269
236
  get message(): string;
270
237
  }
238
+ /**
239
+ * Why one line could not be decoded into an envelope: not JSON, not an
240
+ * envelope (or a bad payload), or a tag the registry does not define.
241
+ *
242
+ * @public
243
+ */
244
+ type DecodeError = MalformedLine | InvalidData | UnknownEvent;
271
245
  /**
272
246
  * Every error this package raises from the pure core and the journal service.
273
247
  *
@@ -301,18 +275,6 @@ type JsonlError = MalformedLine | UnknownEvent | InvalidData | UnserializableDat
301
275
  * @public
302
276
  */
303
277
  type DataSchema = Schema.Codec<unknown, unknown, never, never>;
304
- /**
305
- * Unique type identifier marking a JSONL event definition.
306
- *
307
- * @public
308
- */
309
- export type JsonlEventTypeId = "~effected/jsonl/JsonlEvent";
310
- /**
311
- * Runtime type identifier marking a JSONL event definition.
312
- *
313
- * @public
314
- */
315
- export declare const JsonlEventTypeId: JsonlEventTypeId;
316
278
  /**
317
279
  * One event definition: a tag, the schema its `data` must satisfy, and the two
318
280
  * lifecycle markings.
@@ -325,7 +287,6 @@ export declare const JsonlEventTypeId: JsonlEventTypeId;
325
287
  * @public
326
288
  */
327
289
  export interface JsonlEvent<out Tag extends string, in out Data extends DataSchema = Schema.Codec<void, void, never, never>, out Terminal extends boolean = false, out Reopen extends boolean = false> {
328
- readonly [JsonlEventTypeId]: JsonlEventTypeId;
329
290
  /** The string tag: the envelope discriminant and the primary filter key. */
330
291
  readonly tag: Tag;
331
292
  /** The schema the envelope's `data` is validated against. */
@@ -356,7 +317,6 @@ export declare namespace JsonlEvent {
356
317
  * @public
357
318
  */
358
319
  interface Any {
359
- readonly [JsonlEventTypeId]: JsonlEventTypeId;
360
320
  readonly tag: string;
361
321
  readonly data: DataSchema;
362
322
  readonly terminal: boolean;
@@ -374,14 +334,6 @@ export declare namespace JsonlEvent {
374
334
  }>;
375
335
  /** The decoded payload type registered for a given tag. */
376
336
  type Data<R extends Registry, T extends string> = WithTag<R, T>["data"]["Type"];
377
- /** The tags marked `terminal` in a registry. */
378
- type TerminalTags<R extends Registry> = Extract<Events<R>, {
379
- readonly terminal: true;
380
- }>["tag"];
381
- /** The tags marked `reopen` in a registry. */
382
- type ReopenTags<R extends Registry> = Extract<Events<R>, {
383
- readonly reopen: true;
384
- }>["tag"];
385
337
  }
386
338
  /**
387
339
  * Defines an event.
@@ -419,33 +371,92 @@ export declare const JsonlEvent: {
419
371
  }) => JsonlEvent<Tag, Data, Terminal, Reopen>;
420
372
  };
421
373
  //#endregion
422
- //#region src/Envelope.d.ts
374
+ //#region src/LineSlice.d.ts
423
375
  /**
424
- * Stage one of the read path: the envelope with its payload left undecoded.
376
+ * A single candidate line: its text, and where it lives in the source **in
377
+ * bytes**.
378
+ *
379
+ * Every offset here is a UTF-8 byte offset, never a UTF-16 code-unit index,
380
+ * because these values are cursors into a file that persist across process
381
+ * restarts. A `String.length`-derived offset is correct only for ASCII journals
382
+ * and is the single most likely bug in this module.
383
+ *
384
+ * The terminator is **not** part of the content: `text` and `length` exclude
385
+ * the trailing `\n`, and exclude the `\r` of a `\r\n` pair. `end` includes it,
386
+ * which is why `end - offset` is not always `length`.
425
387
  *
426
- * A `Schema.Struct` rather than a `Schema.Class` deliberately. This is the hot
427
- * path — it runs for every line of every read, including the lines a `Slice` is
428
- * about to discard — and it is a transient the caller never holds, so there is
429
- * nothing to gain from wrapping each one in a class instance.
388
+ * A plain record rather than a class: `Line.split` produces one per line of
389
+ * every read, and a class instance costs some forty times a plain object on
390
+ * that path. The schema exists so errors can carry the line structurally.
430
391
  *
431
- * `data` is `Schema.Unknown`, which passes the payload through without
432
- * traversing it. That is what keeps frame decoding independent of payload
433
- * depth, and what lets filtering happen before any payload schema runs.
392
+ * @example
393
+ * ```ts
394
+ * import { Line } from "@effected/jsonl";
395
+ *
396
+ * const [first] = Line.split('{"a":1}\r\n{"b":2}\n');
397
+ * first?.text; // '{"a":1}'
398
+ * first?.offset; // 0
399
+ * first?.length; // 7 — content bytes, no terminator
400
+ * first?.end; // 9 — past the CR and the LF
401
+ * first?.terminated; // true
402
+ * ```
434
403
  *
435
404
  * @public
436
405
  */
437
- export declare const EnvelopeFrame: Schema.Struct<{
438
- readonly at: Schema.DateTimeUtcFromString;
439
- readonly event: Schema.String;
440
- readonly scope: Schema.optionalKey<Schema.String>;
441
- readonly data: Schema.Unknown;
406
+ export declare const LineSlice: Schema.Struct<{
407
+ /** UTF-8 byte offset of this line's first content byte. */
408
+ readonly offset: Schema.Number;
409
+ /**
410
+ * UTF-8 byte offset just past this line's terminator — the offset at which
411
+ * the next line begins, and the resume cursor for an incremental read.
412
+ *
413
+ * Equal to `offset + length` when the line is unterminated.
414
+ */
415
+ readonly end: Schema.Number;
416
+ /** UTF-8 byte length of `text`, excluding any terminator. */
417
+ readonly length: Schema.Number;
418
+ /** The line's content, with its terminator and any paired `\r` removed. */
419
+ readonly text: Schema.String;
420
+ /**
421
+ * Whether a `\n` terminated this line in the source.
422
+ *
423
+ * `false` can only occur on the final line, and means the line **may be a
424
+ * torn tail** — a writer caught mid-append.
425
+ */
426
+ readonly terminated: Schema.Boolean;
442
427
  }>;
443
428
  /**
444
- * A decoded envelope: the frame, its validated payload, and where it sits in
445
- * the source.
429
+ * A single candidate line: its text, and where it lives in the source in bytes.
446
430
  *
447
- * This interface is what the "derived discriminated union" derives *into*.
448
- * A registry of event definitions produces a union of these discriminated on
431
+ * @public
432
+ */
433
+ export type LineSlice = typeof LineSlice.Type;
434
+ /**
435
+ * Where an envelope's line sits in the journal, in UTF-8 bytes.
436
+ *
437
+ * All an envelope keeps of its source line: enough to resume from (`end`) or
438
+ * to go back and read the line again (`offset`). The line's text is not kept —
439
+ * the envelope already holds its decoded payload, and a second copy of every
440
+ * payload as raw JSON would double what a buffered stream holds. Errors keep
441
+ * the full {@link (LineSlice:type)}, because there the text is the evidence.
442
+ *
443
+ * @public
444
+ */
445
+ interface LinePosition {
446
+ /** Byte offset of the line's first content byte. */
447
+ readonly offset: number;
448
+ /**
449
+ * Byte offset just past the line's terminator — where the next line begins.
450
+ * Persist this and pass it back as a slice's `cursor` to resume.
451
+ */
452
+ readonly end: number;
453
+ }
454
+ //#endregion
455
+ //#region src/Envelope.d.ts
456
+ /**
457
+ * A decoded envelope: the frame, its validated payload, and where its line sits.
458
+ *
459
+ * A registry of event definitions derives a union of these discriminated on
449
460
  * `event`, so narrowing on the tag recovers the payload type registered for it.
450
461
  *
451
462
  * @public
@@ -459,8 +470,8 @@ export interface Envelope<out Tag extends string, out Data> {
459
470
  readonly scope?: string | undefined;
460
471
  /** The payload, validated against the schema registered for `event`. */
461
472
  readonly data: Data;
462
- /** Where this envelope's line sits in the source, in bytes. */
463
- readonly line: LineSlice;
473
+ /** Where this envelope's line sits in the journal. `position.end` is the resume cursor. */
474
+ readonly position: LinePosition;
464
475
  }
465
476
  /**
466
477
  * The envelope type derived from one event definition. Distributes over a union
@@ -486,24 +497,23 @@ type EnvelopeWithTag<R extends JsonlEvent.Registry, T extends string> = Extract<
486
497
  /**
487
498
  * Decoding, encoding and the envelope-level walk-back.
488
499
  *
489
- * Every operation here is synchronous and `Result`-based; the `Effect` forms are
490
- * defined **in terms of** the sync ones via `Effect.fromResult`, so the two can
491
- * never drift into two implementations of the same rule.
500
+ * Every operation is synchronous and `Result`-based, so a hook script can read
501
+ * a journal with no Effect runtime. Lift one into an `Effect` with
502
+ * `Effect.fromResult` where a program needs it.
492
503
  *
493
504
  * @example
494
505
  * ```ts
495
- * import { Envelope, JsonlEvent, Line } from "@effected/jsonl";
496
- * import { Result, Schema } from "effect";
506
+ * import { Envelope, JsonlEvent } from "@effected/jsonl";
507
+ * import { Option, Schema } from "effect";
497
508
  *
498
- * const events = [JsonlEvent.make("started", { data: Schema.Struct({ id: Schema.String }) })] as const;
509
+ * const events = [JsonlEvent.make("state", { data: Schema.Struct({ round: Schema.Number }) })] as const;
499
510
  *
500
511
  * declare const sourceText: string;
501
512
  *
502
- * for (const line of Line.split(sourceText)) {
503
- * const decoded = Envelope.decodeResult(events, line);
504
- * if (Result.isSuccess(decoded) && decoded.success.event === "started") {
505
- * decoded.success.data.id; // typed from the registered payload schema
506
- * }
513
+ * // The whole read path for a snapshot journal, with no runtime.
514
+ * const current = Envelope.lastValid(events, sourceText);
515
+ * if (Option.isSome(current)) {
516
+ * current.value.data.round; // typed from the registered payload schema
507
517
  * }
508
518
  * ```
509
519
  *
@@ -511,100 +521,58 @@ type EnvelopeWithTag<R extends JsonlEvent.Registry, T extends string> = Extract<
511
521
  */
512
522
  export declare const Envelope: {
513
523
  /**
514
- * Stage one only: decode the frame, leaving `data` untouched.
515
- *
516
- * This is what filtering reads. It never runs a payload schema, so its cost
517
- * is independent of payload size and depth, and a line destined to be
518
- * filtered out never pays for its payload at all.
519
- *
520
- * @param line - A slice from `Line.split`.
521
- * @returns The frame with its source `line` attached, or `MalformedLine`
522
- * when the line is not JSON, or `InvalidData` when it is JSON that is not
523
- * an envelope frame.
524
- */
525
- readonly frameResult: (line: LineSlice) => Result.Result<typeof EnvelopeFrame.Type & {
526
- readonly line: LineSlice;
527
- }, MalformedLine | InvalidData>;
528
- /**
529
- * Decode one line into a fully validated envelope, running both stages.
524
+ * Decode one line into a fully validated envelope.
530
525
  *
531
526
  * @param events - The registry defining which tags are legal and what their
532
527
  * payloads must look like.
533
528
  * @param line - A slice from `Line.split`.
534
- * @returns The envelope, or the typed reason it is not one: not JSON
535
- * (`MalformedLine`), not an envelope or a bad payload (`InvalidData`), or
536
- * a tag this registry does not define (`UnknownEvent`).
537
- */
538
- readonly decodeResult: <const R extends JsonlEvent.Registry>(events: R, line: LineSlice) => Result.Result<EnvelopeUnion<R>, MalformedLine | InvalidData | UnknownEvent>;
539
- /**
540
- * Decode a line only if its **frame** is selected, running the payload schema
541
- * for selected lines only.
542
- *
543
- * This is the structural form of the filter-before-decode guarantee: when
544
- * `select` rejects the frame, the registered `data` schema is never reached,
545
- * so a consumer reading a journal it shares with a noisy neighbour pays
546
- * nothing for the neighbour's payloads. Writing the filter *after* a full
547
- * decode would produce identical output while charging for every line — which
548
- * is why this exists as its own operation rather than as advice.
549
- *
550
- * @param events - The registry to validate against.
551
- * @param line - A slice from `Line.split`.
552
- * @param select - Predicate over the decoded frame — `at`, `event`, `scope`
553
- * and the raw undecoded `data`.
554
- * @returns `Option.none()` when the frame is not selected; otherwise the
555
- * fully decoded envelope, or the typed reason it is not one.
529
+ * @returns The envelope, or why the line is not one.
556
530
  */
557
- readonly decodeSelectedResult: <const R extends JsonlEvent.Registry>(events: R, line: LineSlice, select: (frame: typeof EnvelopeFrame.Type) => boolean) => Option.Option<Result.Result<EnvelopeUnion<R>, MalformedLine | InvalidData | UnknownEvent>>;
531
+ readonly decodeResult: <const R extends JsonlEvent.Registry>(events: R, line: LineSlice) => Result.Result<EnvelopeUnion<R>, DecodeError>;
558
532
  /**
559
533
  * Decode every non-blank line, reporting failures rather than dropping them.
560
534
  *
561
- * Same contract as `Line.parseAll` one layer down: a line that is not a
562
- * legal envelope is a `Result` in the array, never a gap in it.
535
+ * A line that is not a legal envelope is a `Result` in the array, never a gap
536
+ * in it: whether a hole is tolerable is the caller's decision.
563
537
  *
564
538
  * @param events - The registry to validate against.
565
539
  * @param text - JSONL source text.
540
+ * @param base - The byte offset `text` starts at in its file.
566
541
  * @returns One `Result` per non-blank line, in file order.
567
542
  */
568
- readonly decodeAllResult: <const R extends JsonlEvent.Registry>(events: R, text: string) => ReadonlyArray<Result.Result<EnvelopeUnion<R>, MalformedLine | InvalidData | UnknownEvent>>;
543
+ readonly decodeAllResult: <const R extends JsonlEvent.Registry>(events: R, text: string, base?: number) => ReadonlyArray<Result.Result<EnvelopeUnion<R>, DecodeError>>;
569
544
  /**
570
- * Walk back from the end to the last valid **envelope**.
545
+ * Walk back from the end to the last valid **envelope** — the journal's
546
+ * current state.
571
547
  *
572
- * This is the binding definition of "the last valid line", and the envelope
573
- * layer is where it has to live. `Line.lastValid` stops at JSON validity,
574
- * which is not enough: a torn scalar tail (`42` cut mid-write leaves `4`)
575
- * parses cleanly as a different value, so a JSON-level walk-back would hand
576
- * back corruption as the current state. An envelope is always an object,
577
- * every strict prefix of an object text is invalid JSON, and a bare `4` is
578
- * not an envelope — so this walk-back detects the torn tail that the one
579
- * below it structurally cannot.
580
- *
581
- * Decoding stops at the first success, so the cost tracks the damage at the
582
- * tail rather than the age of the file.
548
+ * A torn tail is stepped over, and so is any number of malformed, foreign or
549
+ * blank trailing lines. Validity is judged at the envelope, not merely as
550
+ * JSON: a torn *scalar* (`42` cut mid-write leaves `4`) parses cleanly as a
551
+ * different value, but is not an envelope. Decoding stops at the first
552
+ * success, so the cost tracks the damage at the tail, not the file's age.
583
553
  *
584
554
  * @param events - The registry to validate against.
585
- * @param text - JSONL source text.
555
+ * @param text - JSONL source text — the whole file, or a tail of it that
556
+ * begins at a line boundary.
557
+ * @param base - The byte offset `text` starts at in its file.
586
558
  * @returns The last valid envelope, or `Option.none()` if there is none.
587
559
  */
588
- readonly lastValidResult: <const R extends JsonlEvent.Registry>(events: R, text: string) => Option.Option<EnvelopeUnion<R>>;
560
+ readonly lastValid: <const R extends JsonlEvent.Registry>(events: R, text: string, base?: number) => Option.Option<EnvelopeUnion<R>>;
589
561
  /**
590
562
  * Encode one envelope into a complete JSONL line, terminator included.
591
563
  *
592
564
  * The trailing `\n` is part of the returned string on purpose: the
593
- * cooperative-writer contract is one `writeAll` of a *complete line*, and an
594
- * API that returned the text without its terminator would invite a caller to
595
- * write the two separately — which is exactly the interleaving the contract
596
- * exists to prevent.
597
- *
598
- * No `Schema.Union` is involved. The tag is known, so the payload schema is
599
- * a direct registry lookup; a union would buy nothing and decode eagerly.
565
+ * cooperative-writer contract is one write of a *complete line*, and an API
566
+ * returning the text without its terminator would invite writing the two
567
+ * separately.
600
568
  *
601
569
  * @param events - The registry to validate against.
602
- * @param envelope - The event tag, its payload, the service-assigned
603
- * timestamp, and an optional scope.
604
- * @returns The encoded line, or `UnknownEvent` for a tag the registry does
605
- * not define, `InvalidData` when the payload does not satisfy its schema,
606
- * or `UnserializableData` when the encoded payload cannot be turned into
607
- * JSON (a bigint or a reference cycle).
570
+ * @param envelope - The event tag, its payload, the timestamp, and an
571
+ * optional scope.
572
+ * @returns The encoded line, or `InvalidData` when the payload does not
573
+ * satisfy its schema, `UnserializableData` when the encoded payload is not
574
+ * JSON (a bigint or a reference cycle), or `UnknownEvent` for a tag the
575
+ * registry does not define.
608
576
  */
609
577
  readonly encodeResult: <const R extends JsonlEvent.Registry, const T extends JsonlEvent.Tag<R>>(events: R, envelope: {
610
578
  readonly event: T;
@@ -612,35 +580,67 @@ export declare const Envelope: {
612
580
  readonly at: DateTime.Utc;
613
581
  readonly scope?: string | undefined;
614
582
  }) => Result.Result<string, UnknownEvent | InvalidData | UnserializableData>;
583
+ };
584
+ //#endregion
585
+ //#region src/JournalWatcher.d.ts
586
+ /**
587
+ * The service shape behind {@link JournalWatcher}.
588
+ *
589
+ * @public
590
+ */
591
+ interface JournalWatcherShape {
615
592
  /**
616
- * `Envelope.decodeResult` in the `Effect` channel.
593
+ * Watch `path`, a journal file or the directory it will be created in.
617
594
  *
618
- * Prefer the sync form in a hook or any caller that has no runtime; this
619
- * exists for programs that already are one. Fails with `MalformedLine`,
620
- * `InvalidData` or `UnknownEvent`; requires nothing.
621
- */
622
- readonly decode: <const R extends JsonlEvent.Registry>(events: R, line: LineSlice) => Effect.Effect<EnvelopeUnion<R>, MalformedLine | InvalidData | UnknownEvent>;
623
- /**
624
- * `Envelope.encodeResult` in the `Effect` channel.
595
+ * The effect succeeds only once the watch is **registered**: every change
596
+ * to `path` after it succeeds is delivered on the returned stream. That
597
+ * ordering is the whole contract — the journal arms a watch, then reads
598
+ * what it missed, then follows the stream, and a change landing between a
599
+ * watch that was merely requested and one that was registered would be
600
+ * lost. The watch lives until the enclosing scope closes. Watching a file,
601
+ * the stream ends once the path is removed or names a different file (a
602
+ * rename over it), because a platform watch follows the inode it was armed
603
+ * on and would otherwise go blind; the journal then re-arms on whatever the
604
+ * path names now. Watching a directory, it does not end for a child
605
+ * appearing or vanishing.
625
606
  *
626
- * Fails with `UnknownEvent`, `InvalidData` or `UnserializableData`; requires
627
- * nothing.
607
+ * Each element is the name the platform reports for the change, which may
608
+ * be a bare basename and may be absent. The journal treats every element as
609
+ * an untyped poke — "re-stat the file" — and never opens what it names.
610
+ *
611
+ * A path that does not exist fails with a `PlatformError` whose reason is
612
+ * `NotFound`.
628
613
  */
629
- readonly encode: <const R extends JsonlEvent.Registry, const T extends JsonlEvent.Tag<R>>(events: R, envelope: {
630
- readonly event: T;
631
- readonly data: JsonlEvent.Data<R, T>;
632
- readonly at: DateTime.Utc;
633
- readonly scope?: string | undefined;
634
- }) => Effect.Effect<string, UnknownEvent | InvalidData | UnserializableData>;
635
- };
614
+ readonly watch: (path: string) => Effect.Effect<Stream.Stream<string | undefined, PlatformError.PlatformError>, PlatformError.PlatformError, Scope.Scope>;
615
+ }
616
+ declare const JournalWatcher_base: Context.ServiceClass<JournalWatcher, "@effected/jsonl/JournalWatcher", JournalWatcherShape>;
617
+ /**
618
+ * How a journal is told its file changed underneath it: a watch that reports
619
+ * when it is armed.
620
+ *
621
+ * Core's `FileSystem.watch` cannot serve here. Its stream registers the
622
+ * platform watch in a fiber it forks, after an asynchronous `stat`, and gives
623
+ * no signal when that has happened — so a journal reading what it missed
624
+ * before the watch was live cannot know whether a concurrent append landed
625
+ * before registration (and is lost for good) or after (and will be reported).
626
+ * Under load that window is real. This service closes it by construction:
627
+ * {@link JournalWatcherShape.watch} succeeds only once registration is done.
628
+ *
629
+ * The library owns no backend. On Node, provide `NodeJournalWatcher.layer`
630
+ * from `@effected/jsonl/node`; elsewhere, implement the shape over the
631
+ * platform's own watch primitive, honouring the arm-before-success contract.
632
+ *
633
+ * @public
634
+ */
635
+ export declare class JournalWatcher extends JournalWatcher_base {}
636
636
  //#endregion
637
637
  //#region src/Slice.d.ts
638
638
  /**
639
- * A filter over envelope fields.
639
+ * A filter over envelope fields, plus where to resume and what to do with a
640
+ * line that cannot be decoded.
640
641
  *
641
- * Every field is optional and they combine with AND. An omitted field does not
642
- * filter; an empty `events: []` or `scopes: []` matches nothing, which is the
643
- * honest reading of "restrict to this empty set" and is tested.
642
+ * Every field is optional and the filters combine with AND. An omitted field
643
+ * does not filter; an empty `events: []` or `scopes: []` matches nothing.
644
644
  *
645
645
  * @public
646
646
  */
@@ -658,58 +658,98 @@ interface Slice<R extends JsonlEvent.Registry, T extends JsonlEvent.Tag<R> = Jso
658
658
  /** Lower bound on `at`, **inclusive**. */
659
659
  readonly from?: DateTime.Utc | undefined;
660
660
  /**
661
- * Upper bound on `at`, **exclusive**.
662
- *
663
- * Half-open so adjacent windows tile without double-delivering an envelope on
664
- * the seam — which matters because `at` collisions are ordinary at
665
- * millisecond resolution and universal under `TestClock`.
661
+ * Upper bound on `at`, **exclusive**, so adjacent windows tile without
662
+ * double-delivering an envelope on the seam.
666
663
  */
667
664
  readonly to?: DateTime.Utc | undefined;
665
+ /**
666
+ * Resume from this logical byte offset, **inclusive**.
667
+ *
668
+ * Persist a processed envelope's `position.end` and pass it back to replay
669
+ * exactly the remainder. A cursor pointing into a line skips that line whole.
670
+ */
671
+ readonly cursor?: number | undefined;
672
+ /**
673
+ * What to do with a line that cannot be decoded — not JSON, not an envelope,
674
+ * an unknown tag, or a payload its schema rejects.
675
+ *
676
+ * - `"skip"` (the default): leave it out and keep reading. A journal shared
677
+ * with another writer, or with an older or newer version of this one, stays
678
+ * readable past lines this registry does not understand.
679
+ * - `"fail"`: end the stream with the typed error.
680
+ *
681
+ * A line whose frame decodes but does not match the slice is never decoded
682
+ * further, so it cannot fail a stream that would not have delivered it. One
683
+ * that cannot be framed at all counts against every slice.
684
+ */
685
+ readonly onInvalid?: "skip" | "fail" | undefined;
686
+ }
687
+ //#endregion
688
+ //#region src/internal/engine.d.ts
689
+ /**
690
+ * Options for one append.
691
+ *
692
+ * @public
693
+ */
694
+ interface AppendOptions {
695
+ /** The partition key to stamp on the envelope. */
696
+ readonly scope?: string | undefined;
668
697
  }
669
698
  /**
670
- * A {@link Slice} plus a resume point.
699
+ * Configuration for one journal.
671
700
  *
672
701
  * @public
673
702
  */
674
- interface CursoredSlice<R extends JsonlEvent.Registry, T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>> extends Slice<R, T> {
703
+ interface JournalConfig {
704
+ /** Path to the journal file. It need not exist yet. */
705
+ readonly path: string;
675
706
  /**
676
- * Resume from this logical byte offset, **inclusive**.
707
+ * The directory to watch while the journal does not exist yet.
677
708
  *
678
- * Offsets are post-BOM, matching every offset this package emits. Persist a
679
- * processed envelope's `line.end` and pass it back to replay exactly the
680
- * remainder: `end` is the start of the next line, so nothing is redelivered
681
- * and nothing is skipped.
709
+ * Defaults to `path`'s parent — everything before its last `/` or `\`, the
710
+ * one piece of path arithmetic this package performs. Name it explicitly when
711
+ * your path convention does not survive that, such as a drive-relative
712
+ * Windows path. Used only until the journal exists.
682
713
  */
683
- readonly cursor?: number | undefined;
714
+ readonly directory?: string | undefined;
715
+ /**
716
+ * Capacity of the internal subscriber hub. Bounded with backpressure: a slow
717
+ * subscriber slows appends rather than silently missing envelopes.
718
+ */
719
+ readonly capacity?: number | undefined;
720
+ /**
721
+ * How long scope close waits for subscribers to accept the end of the
722
+ * journal. Defaults to five seconds; a subscriber that never consumes must
723
+ * not be able to hold shutdown hostage.
724
+ */
725
+ readonly shutdownPublishTimeout?: Duration.Input | undefined;
684
726
  }
685
- //#endregion
686
- //#region src/Journal.d.ts
687
727
  /**
688
- * The failure channel of a write operation.
728
+ * Why an append failed.
689
729
  *
690
- * `PlatformError` rides through untranslated per the taxonomy's rule. **Any
691
- * `PlatformError` out of an append must be treated as a possibly-torn tail**:
692
- * `writeAll` reports no byte count, so a failure cannot be read as "nothing was
693
- * written". The next reader walks back over whatever fragment survived.
730
+ * `PlatformError` passes through untranslated. **Treat any `PlatformError` from
731
+ * an append as a possibly-torn tail**: a failed write cannot be read as
732
+ * "nothing was written". The next reader walks back over the fragment.
694
733
  *
695
734
  * @public
696
735
  */
697
- type JournalWriteError = JournalClosed | JournalNotFound | TerminalViolation | UnknownEvent | MalformedLine | InvalidData | UnserializableData | PlatformError.PlatformError;
736
+ type AppendError = JournalClosed | JournalNotFound | TerminalViolation | InvalidData | UnserializableData | PlatformError.PlatformError;
698
737
  /**
699
- * The failure channel of a read operation.
738
+ * Why a historical read failed. A {@link DecodeError} only arrives when the
739
+ * slice asks for it with `onInvalid: "fail"`.
700
740
  *
701
741
  * @public
702
742
  */
703
- type JournalReadError = JournalNotFound | PlatformError.PlatformError;
743
+ type QueryError = JournalNotFound | DecodeError | PlatformError.PlatformError;
704
744
  /**
705
- * Options for one append.
745
+ * Why a live read failed: anything a query can fail with, plus the file being
746
+ * truncated or replaced beneath the subscriber.
706
747
  *
707
748
  * @public
708
749
  */
709
- interface AppendOptions {
710
- /** The partition key to stamp on the envelope. */
711
- readonly scope?: string | undefined;
712
- }
750
+ type ChangesError = QueryError | JournalResync;
751
+ //#endregion
752
+ //#region src/Journal.d.ts
713
753
  /**
714
754
  * The shape of a `Journal`, typed by its registry.
715
755
  *
@@ -723,250 +763,162 @@ interface JournalShape<R extends JsonlEvent.Registry> {
723
763
  * ordering does not depend on two writers agreeing about the time, and
724
764
  * `TestClock` controls it exactly in tests.
725
765
  */
726
- readonly append: <T extends JsonlEvent.Tag<R>>(event: T, data: JsonlEvent.Data<R, T>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>, JournalWriteError>;
766
+ readonly append: <T extends JsonlEvent.Tag<R>>(event: T, data: JsonlEvent.Data<R, T>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>, AppendError>;
727
767
  /**
728
- * Inherit-and-patch: read the last valid envelope, shallow-merge `patch`
729
- * over its `data`, validate the result and append it.
768
+ * Inherit-and-patch: shallow-merge `patch` over the current state's `data`,
769
+ * validate the result and append it — read and write under one lock.
730
770
  *
731
- * This is the snapshot-journal primitive — each line is a complete state and
732
- * most transitions change one field. The merge is **shallow** by decision: a
733
- * nested object in the patch replaces the one beneath it.
771
+ * The snapshot-journal primitive: each line is a complete state and most
772
+ * transitions change one field. The merge is **shallow**: a nested object in
773
+ * the patch replaces the one beneath it.
734
774
  */
735
- readonly appendPatch: <T extends JsonlEvent.Tag<R>>(event: T, patch: Partial<JsonlEvent.Data<R, T>>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>, JournalWriteError>;
775
+ readonly appendPatch: <T extends JsonlEvent.Tag<R>>(event: T, patch: Partial<JsonlEvent.Data<R, T>>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>, AppendError>;
736
776
  /**
737
- * The current last valid **envelope**, as an observable `Option`.
777
+ * The journal's current state: its last valid **envelope**.
738
778
  *
739
- * "Last valid" always means the last valid envelope, never merely the last
740
- * valid JSON — a torn scalar tail parses as a different value and only the
741
- * envelope contract detects it.
779
+ * "Last valid" means the last valid envelope, never merely the last valid
780
+ * JSON. Kept current by local appends and by the watcher.
742
781
  */
743
- readonly latest: SubscriptionRef.SubscriptionRef<Option.Option<EnvelopeUnion<R>>>;
782
+ readonly latest: Effect.Effect<Option.Option<EnvelopeUnion<R>>>;
783
+ /** {@link JournalShape.latest} as a stream: the current value, then every change. */
784
+ readonly latestChanges: Stream.Stream<Option.Option<EnvelopeUnion<R>>>;
744
785
  /**
745
786
  * Whether the journal is quiescent — its tail is an event marked `terminal`.
746
- *
747
- * Derived from {@link JournalShape.latest} rather than tracked separately, so
748
- * the two cannot disagree.
787
+ * Derived from {@link JournalShape.latest}, so the two cannot disagree.
749
788
  */
750
789
  readonly quiescent: Effect.Effect<boolean>;
751
790
  /**
752
791
  * Historical read: a finite `Stream` of the envelopes matching `slice`.
753
792
  *
754
- * Every element carries its logical byte offsets on `line`, so iteration is
755
- * resumable across process restarts by persisting `line.end` and passing it
756
- * back as `cursor`.
757
- *
758
- * Filtering happens on the envelope **frame**, strictly before the payload
759
- * schema runs, so a non-matching line's `data` is never decoded.
793
+ * Resumable across restarts: persist a processed envelope's `position.end`
794
+ * and pass it back as `cursor`. Filtering happens on the envelope frame
795
+ * before the payload schema runs, so a non-matching line's `data` is never
796
+ * decoded.
760
797
  *
761
798
  * @remarks
762
- * **Cost, stated rather than implied.** As built, the requested region —
763
- * `cursor` to the end of the file — is read in ONE allocation bounded by the
764
- * file's size, and the matching envelopes are buffered before the first is
765
- * emitted. An unsliced `query()` over a large journal therefore does hold
766
- * that journal in memory; a `cursor` is what bounds the read, which is
767
- * precisely what a resuming consumer already persists. The window-bounded
768
- * reads are `latest` and the `lastValid`-backed paths. Emitting per window,
769
- * so an unsliced query costs a window rather than a file, is tracked as
770
- * spencerbeggs/effected#233.
799
+ * The read is paged: the region from `cursor` to the end of the file as of
800
+ * the call is read a bounded page at a time, each page's matches emitted
801
+ * before the next is read. Memory is a page plus the longest line, and a
802
+ * consumer that stops early stops the reading. A line is read once its `\n`
803
+ * lands: an unterminated tail may be a writer mid-append, so it is left out
804
+ * rather than rejected.
771
805
  */
772
- readonly query: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?: CursoredSlice<R, T> | undefined) => Stream.Stream<EnvelopeWithTag<R, T>, JournalReadError | JsonlError>;
806
+ readonly query: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?: Slice<R, T> | undefined) => Stream.Stream<EnvelopeWithTag<R, T>, QueryError>;
773
807
  /**
774
- * Live read: a `Stream` of matching envelopes as they are appended.
808
+ * Live read: matching envelopes as they are appended, by this process or any
809
+ * other writer.
775
810
  *
776
- * With a `cursor`, replay-from-cursor and the live tail are **one seam**: the
777
- * replayed history and the live tail are the same stream, filtered the same
778
- * way, so a consumer cannot observe a gap or a duplicate at the join.
779
- *
780
- * The stream **ends** — it never hangs — when the journal becomes quiescent
781
- * (a `terminal` event reaches the tail) or its scope closes. Subscribing to
782
- * an already-quiescent journal ends immediately.
811
+ * With a `cursor`, replay and live tail are **one seam**: no gap and no
812
+ * duplicate at the join. The stream **ends** — it never hangs — when the
813
+ * journal becomes quiescent or its scope closes, and fails with
814
+ * `JournalResync` if the file is truncated or replaced beneath it.
783
815
  *
784
816
  * @remarks
785
- * Two properties are worth knowing before relying on them.
786
- *
787
- * The **delivered-before-end guarantee is scoped to consuming subscribers**:
788
- * every append that completed is delivered before the stream ends, provided
789
- * the subscriber keeps taking. One that stops taking cannot observe
790
- * completion by definition, and shutdown will not wait on it past its bound.
791
- *
792
- * The **filter-before-decode guarantee is structural on the historical half
793
- * only**. Replayed lines are filtered on the frame before their payload is
794
- * decoded. Live envelopes arrive already decoded — the appender decoded its
795
- * own payload in order to return it — so a filtered-out live envelope was
796
- * decoded once, by the writer, not by each subscriber.
817
+ * Every append that completed is delivered before the stream ends, provided
818
+ * the subscriber keeps taking; shutdown will not wait past its bound for one
819
+ * that does not.
797
820
  */
798
- readonly changes: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?: CursoredSlice<R, T> | undefined) => Stream.Stream<EnvelopeWithTag<R, T>, JournalReadError | JsonlError>;
821
+ readonly changes: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?: Slice<R, T> | undefined) => Stream.Stream<EnvelopeWithTag<R, T>, ChangesError>;
799
822
  /**
800
- * A running fold over a slice, emitted as it advances.
801
- *
802
- * The fold only ever sees its own slice, so a per-scope state machine over a
803
- * shared journal is exhaustively checkable against just that scope's events.
823
+ * A running fold over {@link JournalShape.changes}, emitted as it advances.
824
+ * The fold only sees its own slice, so a per-scope state machine is
825
+ * exhaustively checkable against that scope's events alone.
804
826
  */
805
- readonly projection: <S, T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(initial: S, fold: (state: S, envelope: EnvelopeWithTag<R, T>) => S, slice?: CursoredSlice<R, T> | undefined) => Stream.Stream<S, JournalReadError | JsonlError>;
806
- /** Create the journal file if it does not exist. Always explicit. */
827
+ readonly projection: <S, T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(initial: S, fold: (state: S, envelope: EnvelopeWithTag<R, T>) => S, slice?: Slice<R, T> | undefined) => Stream.Stream<S, ChangesError>;
828
+ /** Create the journal file if it does not exist. Creation is always explicit. */
807
829
  readonly create: Effect.Effect<void, PlatformError.PlatformError>;
808
- /** Remove the journal file. Always explicit. */
830
+ /** Remove the journal file. */
809
831
  readonly remove: Effect.Effect<void, PlatformError.PlatformError>;
810
- /**
811
- * The internal hub, exposed for the read surfaces built over it.
812
- *
813
- * Elements are `Take` chunks and end-of-stream is a published `Exit`, which
814
- * is what lets quiescence and graceful shutdown arrive at a subscriber as a
815
- * normal stream end. Never shut this down to signal completion —
816
- * `PubSub.shutdown` *interrupts* subscribers, which tears an in-flight
817
- * stream instead of ending it.
818
- *
819
- * @internal
820
- */
821
- readonly hub: PubSub.PubSub<Take.Take<EnvelopeUnion<R>, JsonlError>>;
822
832
  }
823
833
  /**
824
- * Configuration for one journal layer.
834
+ * A per-registry `Journal` service class.
825
835
  *
826
836
  * @public
827
837
  */
828
- interface JournalConfig {
829
- /** Path to the journal file. It need not exist yet. */
830
- readonly path: string;
838
+ interface JournalClass<Self, Id extends string, R extends JsonlEvent.Registry, E, RC> extends Context.ServiceClass<Self, Id, JournalShape<R>> {
839
+ /** The registry this journal was defined over. */
840
+ readonly events: R;
831
841
  /**
832
- * The directory to watch while the journal does not exist yet.
842
+ * The layer for this journal, built from the definition's `config`.
833
843
  *
834
- * Defaults to `path`'s parent, derived by taking everything before its last
835
- * `/` or `\`. That derivation is the one piece of path arithmetic this
836
- * package performs, and it is here so the common case needs no second field;
837
- * name the directory explicitly when your path convention does not survive
838
- * it — a drive-relative Windows path, or a path whose parent is not a plain
839
- * prefix of it.
844
+ * One value per class, so providing it twice provides one journal twice —
845
+ * never two unsynchronized journals over the same file.
840
846
  *
841
- * It is used for exactly one thing: `FileSystem.watch` on a journal that has
842
- * not been created yet. Once the journal exists the watch moves to the file
843
- * itself and this value is not consulted again.
844
- */
845
- readonly directory?: string | undefined;
846
- /**
847
- * Capacity of the internal subscriber hub. Bounded with backpressure: the
848
- * journal never drops an envelope for a slow subscriber, so slowness shows
849
- * up at the producer where it is visible rather than as a silent gap in
850
- * somebody's subscription.
847
+ * @remarks
848
+ * A journal file that does not exist yet is legal and constructs cleanly; one
849
+ * that exists and cannot be read fails with `PlatformError`.
851
850
  */
852
- readonly capacity?: number | undefined;
851
+ readonly layer: Layer.Layer<Self, PlatformError.PlatformError | E, FileSystem.FileSystem | JournalWatcher | RC>;
853
852
  /**
854
- * How long scope close waits for the terminal `Exit` to be accepted.
855
- *
856
- * Defaults to five seconds. Bounded rather than indefinite because a
857
- * subscriber that never consumes cannot observe completion by definition,
858
- * and must not be able to hold shutdown hostage.
853
+ * Build a journal over an explicit config, for a path only known at run
854
+ * time. Wrap it in `Layer.effect(Class, Class.make(config))` and bind that
855
+ * layer once: every build is an independent journal.
859
856
  */
860
- readonly shutdownPublishTimeout?: Duration.Input | undefined;
857
+ readonly make: (config: JournalConfig) => Effect.Effect<JournalShape<R>, PlatformError.PlatformError, FileSystem.FileSystem | JournalWatcher | Scope.Scope>;
861
858
  }
862
859
  /**
863
- * A per-registry `Journal` service class.
860
+ * Defines journal services.
864
861
  *
865
862
  * @public
866
863
  */
867
- interface JournalClass<Self, Id extends string, R extends JsonlEvent.Registry> extends Context.ServiceClass<Self, Id, JournalShape<R>> {
868
- /** The registry this journal was defined over. */
869
- readonly events: R;
864
+ export declare class Journal {
865
+ private constructor();
870
866
  /**
871
- * Build the layer for this journal.
872
- *
873
- * @remarks
874
- * **Construction can fail with a `PlatformError`, and that is deliberate.** A
875
- * journal file that does not exist yet is a legal state and constructs
876
- * cleanly — but a file that exists and cannot be read (`EACCES`, a bad
877
- * handle) is a real failure, and typing this channel `never` would have made
878
- * it arrive as an untypeable defect that no caller could catch.
867
+ * Define a `Journal` service class over a registry.
879
868
  *
880
- * **Bind the result to a const and provide that const.** Layers are memoized
881
- * by reference, so calling this twice mints **two independent journals over
882
- * one file** — two semaphores, two hubs, two `latest` refs — and their
883
- * appends are not serialized against each other. That is the in-process form
884
- * of exactly the interleaving the cooperative-writer rules exist to prevent,
885
- * and it typechecks perfectly.
869
+ * Each registry gets its own uniquely-keyed class, so several journals
870
+ * coexist in one layer graph, each typed by its own registry. `config` is a
871
+ * plain record or an `Effect` producing one — so a path can come from
872
+ * `Config` or another service, resolved when the layer builds.
886
873
  *
874
+ * @example
887
875
  * ```ts
888
- * // correct
889
- * export const layer = MailJournal.layer({ path });
890
- *
891
- * // WRONG — a second, unserialized journal over the same file
892
- * Effect.provide(program, MailJournal.layer({ path }));
876
+ * import { Journal, JsonlEvent } from "@effected/jsonl";
877
+ * import { NodeJournalWatcher } from "@effected/jsonl/node";
878
+ * import { NodeFileSystem } from "@effect/platform-node";
879
+ * import { Effect, Schema } from "effect";
880
+ *
881
+ * const events = [JsonlEvent.make("mail", { data: Schema.Struct({ round: Schema.Number }) })] as const;
882
+ *
883
+ * class Mail extends Journal.Service<Mail>()("app/Mail", {
884
+ * events,
885
+ * config: { path: ".app/mail.jsonl" },
886
+ * }) {}
887
+ *
888
+ * const program = Effect.gen(function* () {
889
+ * const mail = yield* Mail;
890
+ * yield* mail.append("mail", { round: 1 });
891
+ * }).pipe(
892
+ * Effect.provide(Mail.layer),
893
+ * Effect.provide([NodeFileSystem.layer, NodeJournalWatcher.layer]),
894
+ * );
893
895
  * ```
894
896
  */
895
- readonly layer: (config: JournalConfig) => Layer.Layer<Self, PlatformError.PlatformError, FileSystem.FileSystem>;
896
- }
897
- /**
898
- * Define a `Journal` service class over a registry.
899
- *
900
- * A `Context.Service` cannot itself be generic over the registry — the shape
901
- * binds at declaration and a Key cannot be parameterized at retrieval — so each
902
- * registry gets its own uniquely-keyed class, and several journals coexist in
903
- * one layer graph with each one's operations typed by *its* registry. This
904
- * mirrors `ConfigFile.Service<Self, A>()(id)`.
905
- *
906
- * Pass the registry as `options.events`; the returned class exposes it as
907
- * `events` and builds the scoped layer with `.layer(config)`.
908
- *
909
- * @example
910
- * ```ts
911
- * import { Journal, JsonlEvent } from "@effected/jsonl";
912
- * import { Schema } from "effect";
913
- *
914
- * const events = [JsonlEvent.make("started", { data: Schema.Void })] as const;
915
- *
916
- * class MailJournal extends Journal.Service<MailJournal>()("app/MailJournal", { events }) {}
917
- *
918
- * export const layer = MailJournal.layer({ path: ".app/mail.jsonl" });
919
- * ```
920
- *
921
- * @public
922
- */
923
- export declare const Journal: {
924
- readonly Service: <Self>() => <const Id extends string, const R extends JsonlEvent.Registry>(id: Id, options: {
897
+ static Service<Self>(): <const Id extends string, const R extends JsonlEvent.Registry, E = never, RC = never>(id: Id, options: {
925
898
  readonly events: R;
926
- }) => JournalClass<Self, Id, R>;
927
- };
899
+ readonly config: JournalConfig | Effect.Effect<JournalConfig, E, RC>;
900
+ }) => JournalClass<Self, Id, R, E, RC>;
901
+ }
928
902
  //#endregion
929
903
  //#region src/Line.d.ts
930
- declare const ParsedLine_base: Schema.Class<ParsedLine, Schema.Struct<{
931
- /** Where this line lives in the source. */
932
- readonly line: typeof LineSlice;
933
- /** The parsed JSON value — any JSON value, not necessarily an object. */
934
- readonly value: Schema.Unknown;
935
- }>, {}>;
936
904
  /**
937
- * A line that parsed as JSON, paired with the slice it came from.
905
+ * Splitting and parsing JSONL text.
938
906
  *
939
- * `value` is deliberately `unknown`: this layer knows JSON, not envelopes.
940
- * Validating `event`, `at`, `scope` and the registered payload schema is the
941
- * envelope layer's job, and keeping the split means a malformed *envelope* and
942
- * a malformed *line* stay distinguishable failures.
943
- *
944
- * @public
945
- */
946
- export declare class ParsedLine extends ParsedLine_base {}
947
- /**
948
- * Splitting, parsing and corrupt-tail walk-back over JSONL text.
949
- *
950
- * Every operation is total and synchronous: it returns a value for every
951
- * input, including empty text, torn tails, and text that is not JSONL at all.
952
- * Nothing here throws — a `JSON.parse` failure becomes a {@link MalformedLine}
953
- * in the returned `Result`, because a journal is untrusted input and untrusted
954
- * input fails typed.
907
+ * Every operation is total and synchronous: nothing here throws. A `JSON.parse`
908
+ * failure becomes a {@link MalformedLine}, because a journal is untrusted input
909
+ * and untrusted input fails typed.
955
910
  *
956
911
  * @public
957
912
  */
958
913
  export declare class Line {
914
+ private constructor();
959
915
  /**
960
916
  * The UTF-8 byte length of a string.
961
917
  *
962
- * Exposed because callers doing their own offset arithmetic must measure
963
- * the same way this module does. `String.length` is a different number for
964
- * any non-ASCII line.
965
- *
966
- * An unpaired surrogate counts as the 3 bytes of U+FFFD, matching what a
967
- * UTF-8 write actually emits. The offsets therefore stay true to the file
968
- * even though the character itself cannot survive the round trip — text
969
- * read back from a UTF-8 journal never contains one.
918
+ * Exposed because callers doing their own offset arithmetic must measure the
919
+ * same way this module does. `String.length` is a different number for any
920
+ * non-ASCII line. An unpaired surrogate counts as the 3 bytes of U+FFFD,
921
+ * matching what a UTF-8 write emits.
970
922
  *
971
923
  * @example
972
924
  * ```ts
@@ -980,88 +932,31 @@ export declare class Line {
980
932
  /**
981
933
  * Split text into candidate lines with byte-exact offsets.
982
934
  *
983
- * Lines are separated by `\n`; a `\r` immediately preceding it is treated as
984
- * part of the terminator, so CRLF journals split identically to LF ones. A
985
- * bare `\r` is ordinary content, because only `\n` terminates.
986
- *
987
- * A trailing terminator does **not** produce a phantom empty final line, so
988
- * a well-formed journal yields exactly as many slices as it has entries.
989
- * Interior blank lines *are* returned — they are real bytes at real offsets,
990
- * and hiding them would make the offsets lie.
991
- *
992
- * Only the final slice can have `terminated: false`.
935
+ * Lines are separated by `\n`; a `\r` immediately preceding it is part of the
936
+ * terminator, so CRLF journals split identically to LF ones. A bare `\r` is
937
+ * ordinary content. A trailing terminator does **not** produce a phantom
938
+ * empty final line; interior blank lines *are* returned, because they are
939
+ * real bytes at real offsets. Only the final slice can be unterminated.
993
940
  *
994
941
  * @param text - JSONL source text.
995
- * @returns One {@link LineSlice} per candidate line, in source order.
942
+ * @param base - The byte offset `text` starts at in its file. Every offset
943
+ * returned is shifted by it, so text read from the middle of a journal
944
+ * yields offsets into the journal rather than into the text.
945
+ * @returns One {@link (LineSlice:type)} per candidate line, in source order.
996
946
  */
997
- static split(text: string): ReadonlyArray<LineSlice>;
998
- /**
999
- * The byte offset up to which this text has been fully consumed.
1000
- *
1001
- * This is the offset past the last **terminated** line. An unterminated
1002
- * final line is left unconsumed on purpose: it may be a writer caught
1003
- * mid-append, and re-reading from this offset once the file grows sees the
1004
- * completed line rather than gluing a stale fragment to fresh bytes.
1005
- *
1006
- * @param text - JSONL source text.
1007
- * @returns The resume cursor, in UTF-8 bytes.
1008
- */
1009
- static consumedOffset(text: string): number;
947
+ static split(text: string, base?: number): ReadonlyArray<LineSlice>;
1010
948
  /**
1011
949
  * Parse one candidate line's JSON.
1012
950
  *
1013
- * Any JSON value succeeds — objects, arrays and scalars alike. This layer
1014
- * does not know what an envelope is.
951
+ * Any JSON value succeeds — objects, arrays and scalars alike.
1015
952
  *
1016
953
  * @param line - A slice from {@link Line.split}.
1017
- * @returns The {@link ParsedLine}, or a {@link MalformedLine} carrying the
1018
- * slice so the caller can locate the damage and decide whether an
1019
- * unterminated line is a torn tail worth waiting for.
1020
- */
1021
- static parseResult(line: LineSlice): Result.Result<ParsedLine, MalformedLine>;
1022
- /**
1023
- * Parse every non-blank line, reporting failures rather than dropping them.
1024
- *
1025
- * There is deliberately no "valid lines only" variant. A hole in the middle
1026
- * of a journal is information — it means a line was written that no reader
1027
- * can interpret — and whether that is tolerable is the caller's decision,
1028
- * not this module's. Filter the `Result`s yourself and the choice stays
1029
- * visible at the call site.
1030
- *
1031
- * Whitespace-only lines are skipped rather than reported: they carry no
1032
- * information to lose, and reporting them would make a hand-edited journal
1033
- * look corrupt.
1034
- *
1035
- * @param text - JSONL source text.
1036
- * @returns One `Result` per non-blank line, in source order.
1037
- */
1038
- static parseAll(text: string): ReadonlyArray<Result.Result<ParsedLine, MalformedLine>>;
1039
- /**
1040
- * Walk back from the end to the last line that parses.
1041
- *
1042
- * This is the whole read path for a snapshot-style journal, where the
1043
- * current state *is* the last valid line: a session killed mid-append leaves
1044
- * a partial final line, and the walk-back steps over it — and over any
1045
- * number of malformed or blank trailing lines — to the last line that means
1046
- * something.
1047
- *
1048
- * Parsing stops at the first success, so the cost is proportional to the
1049
- * damage at the tail rather than to the age of the file.
1050
- *
1051
- * **A torn tail is only detectable when its fragment fails to parse.**
1052
- * Every strict prefix of a JSON *object* is invalid JSON, so a truncated
1053
- * `{"event":"unlinked"}` is always caught. A truncated *scalar* is not:
1054
- * `42` cut mid-write leaves `4`, which parses cleanly as a different value.
1055
- * The JSON layer cannot close that hole — the envelope layer can, because
1056
- * `4` is not an envelope, which is one more reason the envelope is the
1057
- * package's contract rather than an option. Check `LineSlice.terminated` on
1058
- * the result when it matters.
1059
- *
1060
- * @param text - JSONL source text.
1061
- * @returns The last parseable line, or `Option.none()` if none parses.
954
+ * @returns The parsed value, or a {@link MalformedLine} carrying the slice so
955
+ * the caller can locate the damage. An unterminated malformed line is a
956
+ * torn tail; a terminated one is a hole that will never heal.
1062
957
  */
1063
- static lastValid(text: string): Option.Option<ParsedLine>;
958
+ static parseResult(line: LineSlice): Result.Result<unknown, MalformedLine>;
1064
959
  }
1065
960
  //#endregion
1066
- export type { AppendOptions, CursoredSlice, DataSchema, EnvelopeOf, EnvelopeUnion, EnvelopeWithTag, JournalClass, JournalConfig, JournalReadError, JournalShape, JournalWriteError, JsonlError, Slice };
961
+ export type { AppendError, AppendOptions, ChangesError, DataSchema, DecodeError, EnvelopeOf, EnvelopeUnion, EnvelopeWithTag, JournalClass, JournalConfig, JournalShape, JournalWatcherShape, JsonlError, LinePosition, QueryError, Slice };
1067
962
  //# sourceMappingURL=index.d.ts.map