@effected/jsonl 0.8.0 → 0.10.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/Envelope.js +95 -155
- package/Journal.js +42 -463
- package/JsonlError.js +14 -18
- package/JsonlEvent.js +1 -8
- package/Line.js +37 -145
- package/LineSlice.js +12 -15
- package/README.md +4 -4
- package/Slice.js +3 -9
- package/index.d.ts +345 -471
- package/index.js +4 -4
- package/internal/engine.js +399 -0
- package/internal/merge.js +0 -9
- package/internal/tail.js +173 -90
- package/internal/utf8.js +0 -9
- package/package.json +2 -2
package/index.d.ts
CHANGED
|
@@ -1,62 +1,14 @@
|
|
|
1
|
-
import { Context, DateTime, Duration, Effect, FileSystem, Layer, Option, PlatformError,
|
|
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:
|
|
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:
|
|
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
|
-
/**
|
|
114
|
-
|
|
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
|
|
168
|
-
* `latest`
|
|
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.
|
|
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/
|
|
374
|
+
//#region src/LineSlice.d.ts
|
|
423
375
|
/**
|
|
424
|
-
*
|
|
376
|
+
* A single candidate line: its text, and where it lives in the source **in
|
|
377
|
+
* bytes**.
|
|
425
378
|
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
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.
|
|
430
383
|
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
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`.
|
|
387
|
+
*
|
|
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.
|
|
391
|
+
*
|
|
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
|
|
438
|
-
|
|
439
|
-
readonly
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
445
|
-
* the source.
|
|
429
|
+
* A single candidate line: its text, and where it lives in the source in bytes.
|
|
446
430
|
*
|
|
447
|
-
*
|
|
448
|
-
|
|
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
|
|
463
|
-
readonly
|
|
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,97 +497,82 @@ 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
|
|
490
|
-
*
|
|
491
|
-
*
|
|
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.
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* ```ts
|
|
506
|
+
* import { Envelope, JsonlEvent } from "@effected/jsonl";
|
|
507
|
+
* import { Option, Schema } from "effect";
|
|
508
|
+
*
|
|
509
|
+
* const events = [JsonlEvent.make("state", { data: Schema.Struct({ round: Schema.Number }) })] as const;
|
|
510
|
+
*
|
|
511
|
+
* declare const sourceText: string;
|
|
512
|
+
*
|
|
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
|
|
517
|
+
* }
|
|
518
|
+
* ```
|
|
492
519
|
*
|
|
493
520
|
* @public
|
|
494
521
|
*/
|
|
495
522
|
export declare const Envelope: {
|
|
496
523
|
/**
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
* This is what filtering reads. It never runs a payload schema, so its cost
|
|
500
|
-
* is independent of payload size and depth, and a line destined to be
|
|
501
|
-
* filtered out never pays for its payload at all.
|
|
502
|
-
*
|
|
503
|
-
* @param line - A slice from `Line.split`.
|
|
504
|
-
*/
|
|
505
|
-
readonly frameResult: (line: LineSlice) => Result.Result<typeof EnvelopeFrame.Type & {
|
|
506
|
-
readonly line: LineSlice;
|
|
507
|
-
}, MalformedLine | InvalidData>;
|
|
508
|
-
/**
|
|
509
|
-
* Decode one line into a fully validated envelope, running both stages.
|
|
524
|
+
* Decode one line into a fully validated envelope.
|
|
510
525
|
*
|
|
511
526
|
* @param events - The registry defining which tags are legal and what their
|
|
512
527
|
* payloads must look like.
|
|
513
528
|
* @param line - A slice from `Line.split`.
|
|
514
|
-
* @returns The envelope, or the
|
|
515
|
-
* (`MalformedLine`), not an envelope or a bad payload (`InvalidData`), or
|
|
516
|
-
* a tag this registry does not define (`UnknownEvent`).
|
|
529
|
+
* @returns The envelope, or why the line is not one.
|
|
517
530
|
*/
|
|
518
|
-
readonly decodeResult: <const R extends JsonlEvent.Registry>(events: R, line: LineSlice) => Result.Result<EnvelopeUnion<R>,
|
|
531
|
+
readonly decodeResult: <const R extends JsonlEvent.Registry>(events: R, line: LineSlice) => Result.Result<EnvelopeUnion<R>, DecodeError>;
|
|
519
532
|
/**
|
|
520
|
-
* Decode
|
|
521
|
-
* for selected lines only.
|
|
533
|
+
* Decode every non-blank line, reporting failures rather than dropping them.
|
|
522
534
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* so a consumer reading a journal it shares with a noisy neighbour pays
|
|
526
|
-
* nothing for the neighbour's payloads. Writing the filter *after* a full
|
|
527
|
-
* decode would produce identical output while charging for every line — which
|
|
528
|
-
* is why this exists as its own operation rather than as advice.
|
|
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.
|
|
529
537
|
*
|
|
530
538
|
* @param events - The registry to validate against.
|
|
531
|
-
* @param
|
|
532
|
-
* @param
|
|
533
|
-
*
|
|
534
|
-
* @returns `Option.none()` when the frame is not selected; otherwise the
|
|
535
|
-
* fully decoded envelope, or the typed reason it is not one.
|
|
536
|
-
*/
|
|
537
|
-
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>>;
|
|
538
|
-
/**
|
|
539
|
-
* Decode every non-blank line, reporting failures rather than dropping them.
|
|
540
|
-
*
|
|
541
|
-
* Same contract as `Line.parseAll` one layer down: a line that is not a
|
|
542
|
-
* legal envelope is a `Result` in the array, never a gap in it.
|
|
539
|
+
* @param text - JSONL source text.
|
|
540
|
+
* @param base - The byte offset `text` starts at in its file.
|
|
541
|
+
* @returns One `Result` per non-blank line, in file order.
|
|
543
542
|
*/
|
|
544
|
-
readonly decodeAllResult: <const R extends JsonlEvent.Registry>(events: R, text: string) => ReadonlyArray<Result.Result<EnvelopeUnion<R>,
|
|
543
|
+
readonly decodeAllResult: <const R extends JsonlEvent.Registry>(events: R, text: string, base?: number) => ReadonlyArray<Result.Result<EnvelopeUnion<R>, DecodeError>>;
|
|
545
544
|
/**
|
|
546
|
-
* Walk back from the end to the last valid **envelope
|
|
547
|
-
*
|
|
548
|
-
* This is the binding definition of "the last valid line", and the envelope
|
|
549
|
-
* layer is where it has to live. `Line.lastValid` stops at JSON validity,
|
|
550
|
-
* which is not enough: a torn scalar tail (`42` cut mid-write leaves `4`)
|
|
551
|
-
* parses cleanly as a different value, so a JSON-level walk-back would hand
|
|
552
|
-
* back corruption as the current state. An envelope is always an object,
|
|
553
|
-
* every strict prefix of an object text is invalid JSON, and a bare `4` is
|
|
554
|
-
* not an envelope — so this walk-back detects the torn tail that the one
|
|
555
|
-
* below it structurally cannot.
|
|
545
|
+
* Walk back from the end to the last valid **envelope** — the journal's
|
|
546
|
+
* current state.
|
|
556
547
|
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
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.
|
|
559
553
|
*
|
|
560
554
|
* @param events - The registry to validate against.
|
|
561
|
-
* @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.
|
|
562
558
|
* @returns The last valid envelope, or `Option.none()` if there is none.
|
|
563
559
|
*/
|
|
564
|
-
readonly
|
|
560
|
+
readonly lastValid: <const R extends JsonlEvent.Registry>(events: R, text: string, base?: number) => Option.Option<EnvelopeUnion<R>>;
|
|
565
561
|
/**
|
|
566
562
|
* Encode one envelope into a complete JSONL line, terminator included.
|
|
567
563
|
*
|
|
568
564
|
* The trailing `\n` is part of the returned string on purpose: the
|
|
569
|
-
* cooperative-writer contract is one
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
* exists to prevent.
|
|
573
|
-
*
|
|
574
|
-
* No `Schema.Union` is involved. The tag is known, so the payload schema is
|
|
575
|
-
* 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.
|
|
576
568
|
*
|
|
577
569
|
* @param events - The registry to validate against.
|
|
578
|
-
* @param envelope - The event tag, its payload, the
|
|
579
|
-
*
|
|
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.
|
|
580
576
|
*/
|
|
581
577
|
readonly encodeResult: <const R extends JsonlEvent.Registry, const T extends JsonlEvent.Tag<R>>(events: R, envelope: {
|
|
582
578
|
readonly event: T;
|
|
@@ -584,31 +580,15 @@ export declare const Envelope: {
|
|
|
584
580
|
readonly at: DateTime.Utc;
|
|
585
581
|
readonly scope?: string | undefined;
|
|
586
582
|
}) => Result.Result<string, UnknownEvent | InvalidData | UnserializableData>;
|
|
587
|
-
/**
|
|
588
|
-
* {@link (Envelope:variable).decodeResult} in the `Effect` channel.
|
|
589
|
-
*
|
|
590
|
-
* Prefer the sync form in a hook or any caller that has no runtime; this
|
|
591
|
-
* exists for programs that already are one.
|
|
592
|
-
*/
|
|
593
|
-
readonly decode: <const R extends JsonlEvent.Registry>(events: R, line: LineSlice) => Effect.Effect<EnvelopeUnion<R>, MalformedLine | InvalidData | UnknownEvent>;
|
|
594
|
-
/**
|
|
595
|
-
* {@link (Envelope:variable).encodeResult} in the `Effect` channel.
|
|
596
|
-
*/
|
|
597
|
-
readonly encode: <const R extends JsonlEvent.Registry, const T extends JsonlEvent.Tag<R>>(events: R, envelope: {
|
|
598
|
-
readonly event: T;
|
|
599
|
-
readonly data: JsonlEvent.Data<R, T>;
|
|
600
|
-
readonly at: DateTime.Utc;
|
|
601
|
-
readonly scope?: string | undefined;
|
|
602
|
-
}) => Effect.Effect<string, UnknownEvent | InvalidData | UnserializableData>;
|
|
603
583
|
};
|
|
604
584
|
//#endregion
|
|
605
585
|
//#region src/Slice.d.ts
|
|
606
586
|
/**
|
|
607
|
-
* A filter over envelope fields
|
|
587
|
+
* A filter over envelope fields, plus where to resume and what to do with a
|
|
588
|
+
* line that cannot be decoded.
|
|
608
589
|
*
|
|
609
|
-
* Every field is optional and
|
|
610
|
-
* filter; an empty `events: []` or `scopes: []` matches nothing
|
|
611
|
-
* honest reading of "restrict to this empty set" and is tested.
|
|
590
|
+
* Every field is optional and the filters combine with AND. An omitted field
|
|
591
|
+
* does not filter; an empty `events: []` or `scopes: []` matches nothing.
|
|
612
592
|
*
|
|
613
593
|
* @public
|
|
614
594
|
*/
|
|
@@ -626,58 +606,98 @@ interface Slice<R extends JsonlEvent.Registry, T extends JsonlEvent.Tag<R> = Jso
|
|
|
626
606
|
/** Lower bound on `at`, **inclusive**. */
|
|
627
607
|
readonly from?: DateTime.Utc | undefined;
|
|
628
608
|
/**
|
|
629
|
-
* Upper bound on `at`, **exclusive
|
|
630
|
-
*
|
|
631
|
-
* Half-open so adjacent windows tile without double-delivering an envelope on
|
|
632
|
-
* the seam — which matters because `at` collisions are ordinary at
|
|
633
|
-
* millisecond resolution and universal under `TestClock`.
|
|
609
|
+
* Upper bound on `at`, **exclusive**, so adjacent windows tile without
|
|
610
|
+
* double-delivering an envelope on the seam.
|
|
634
611
|
*/
|
|
635
612
|
readonly to?: DateTime.Utc | undefined;
|
|
613
|
+
/**
|
|
614
|
+
* Resume from this logical byte offset, **inclusive**.
|
|
615
|
+
*
|
|
616
|
+
* Persist a processed envelope's `position.end` and pass it back to replay
|
|
617
|
+
* exactly the remainder. A cursor pointing into a line skips that line whole.
|
|
618
|
+
*/
|
|
619
|
+
readonly cursor?: number | undefined;
|
|
620
|
+
/**
|
|
621
|
+
* What to do with a line that cannot be decoded — not JSON, not an envelope,
|
|
622
|
+
* an unknown tag, or a payload its schema rejects.
|
|
623
|
+
*
|
|
624
|
+
* - `"skip"` (the default): leave it out and keep reading. A journal shared
|
|
625
|
+
* with another writer, or with an older or newer version of this one, stays
|
|
626
|
+
* readable past lines this registry does not understand.
|
|
627
|
+
* - `"fail"`: end the stream with the typed error.
|
|
628
|
+
*
|
|
629
|
+
* A line whose frame decodes but does not match the slice is never decoded
|
|
630
|
+
* further, so it cannot fail a stream that would not have delivered it. One
|
|
631
|
+
* that cannot be framed at all counts against every slice.
|
|
632
|
+
*/
|
|
633
|
+
readonly onInvalid?: "skip" | "fail" | undefined;
|
|
636
634
|
}
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region src/internal/engine.d.ts
|
|
637
637
|
/**
|
|
638
|
-
*
|
|
638
|
+
* Options for one append.
|
|
639
639
|
*
|
|
640
640
|
* @public
|
|
641
641
|
*/
|
|
642
|
-
interface
|
|
642
|
+
interface AppendOptions {
|
|
643
|
+
/** The partition key to stamp on the envelope. */
|
|
644
|
+
readonly scope?: string | undefined;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Configuration for one journal.
|
|
648
|
+
*
|
|
649
|
+
* @public
|
|
650
|
+
*/
|
|
651
|
+
interface JournalConfig {
|
|
652
|
+
/** Path to the journal file. It need not exist yet. */
|
|
653
|
+
readonly path: string;
|
|
643
654
|
/**
|
|
644
|
-
*
|
|
655
|
+
* The directory to watch while the journal does not exist yet.
|
|
645
656
|
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
657
|
+
* Defaults to `path`'s parent — everything before its last `/` or `\`, the
|
|
658
|
+
* one piece of path arithmetic this package performs. Name it explicitly when
|
|
659
|
+
* your path convention does not survive that, such as a drive-relative
|
|
660
|
+
* Windows path. Used only until the journal exists.
|
|
650
661
|
*/
|
|
651
|
-
readonly
|
|
662
|
+
readonly directory?: string | undefined;
|
|
663
|
+
/**
|
|
664
|
+
* Capacity of the internal subscriber hub. Bounded with backpressure: a slow
|
|
665
|
+
* subscriber slows appends rather than silently missing envelopes.
|
|
666
|
+
*/
|
|
667
|
+
readonly capacity?: number | undefined;
|
|
668
|
+
/**
|
|
669
|
+
* How long scope close waits for subscribers to accept the end of the
|
|
670
|
+
* journal. Defaults to five seconds; a subscriber that never consumes must
|
|
671
|
+
* not be able to hold shutdown hostage.
|
|
672
|
+
*/
|
|
673
|
+
readonly shutdownPublishTimeout?: Duration.Input | undefined;
|
|
652
674
|
}
|
|
653
|
-
//#endregion
|
|
654
|
-
//#region src/Journal.d.ts
|
|
655
675
|
/**
|
|
656
|
-
*
|
|
676
|
+
* Why an append failed.
|
|
657
677
|
*
|
|
658
|
-
* `PlatformError`
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
* written". The next reader walks back over whatever fragment survived.
|
|
678
|
+
* `PlatformError` passes through untranslated. **Treat any `PlatformError` from
|
|
679
|
+
* an append as a possibly-torn tail**: a failed write cannot be read as
|
|
680
|
+
* "nothing was written". The next reader walks back over the fragment.
|
|
662
681
|
*
|
|
663
682
|
* @public
|
|
664
683
|
*/
|
|
665
|
-
type
|
|
684
|
+
type AppendError = JournalClosed | JournalNotFound | TerminalViolation | InvalidData | UnserializableData | PlatformError.PlatformError;
|
|
666
685
|
/**
|
|
667
|
-
*
|
|
686
|
+
* Why a historical read failed. A {@link DecodeError} only arrives when the
|
|
687
|
+
* slice asks for it with `onInvalid: "fail"`.
|
|
668
688
|
*
|
|
669
689
|
* @public
|
|
670
690
|
*/
|
|
671
|
-
type
|
|
691
|
+
type QueryError = JournalNotFound | DecodeError | PlatformError.PlatformError;
|
|
672
692
|
/**
|
|
673
|
-
*
|
|
693
|
+
* Why a live read failed: anything a query can fail with, plus the file being
|
|
694
|
+
* truncated or replaced beneath the subscriber.
|
|
674
695
|
*
|
|
675
696
|
* @public
|
|
676
697
|
*/
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
698
|
+
type ChangesError = QueryError | JournalResync;
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region src/Journal.d.ts
|
|
681
701
|
/**
|
|
682
702
|
* The shape of a `Journal`, typed by its registry.
|
|
683
703
|
*
|
|
@@ -691,247 +711,158 @@ interface JournalShape<R extends JsonlEvent.Registry> {
|
|
|
691
711
|
* ordering does not depend on two writers agreeing about the time, and
|
|
692
712
|
* `TestClock` controls it exactly in tests.
|
|
693
713
|
*/
|
|
694
|
-
readonly append: <T extends JsonlEvent.Tag<R>>(event: T, data: JsonlEvent.Data<R, T>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>,
|
|
714
|
+
readonly append: <T extends JsonlEvent.Tag<R>>(event: T, data: JsonlEvent.Data<R, T>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>, AppendError>;
|
|
695
715
|
/**
|
|
696
|
-
* Inherit-and-patch:
|
|
697
|
-
*
|
|
716
|
+
* Inherit-and-patch: shallow-merge `patch` over the current state's `data`,
|
|
717
|
+
* validate the result and append it — read and write under one lock.
|
|
698
718
|
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
719
|
+
* The snapshot-journal primitive: each line is a complete state and most
|
|
720
|
+
* transitions change one field. The merge is **shallow**: a nested object in
|
|
721
|
+
* the patch replaces the one beneath it.
|
|
702
722
|
*/
|
|
703
|
-
readonly appendPatch: <T extends JsonlEvent.Tag<R>>(event: T, patch: Partial<JsonlEvent.Data<R, T>>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>,
|
|
723
|
+
readonly appendPatch: <T extends JsonlEvent.Tag<R>>(event: T, patch: Partial<JsonlEvent.Data<R, T>>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>, AppendError>;
|
|
704
724
|
/**
|
|
705
|
-
* The current last valid **envelope
|
|
725
|
+
* The journal's current state: its last valid **envelope**.
|
|
706
726
|
*
|
|
707
|
-
* "Last valid"
|
|
708
|
-
*
|
|
709
|
-
* envelope contract detects it.
|
|
727
|
+
* "Last valid" means the last valid envelope, never merely the last valid
|
|
728
|
+
* JSON. Kept current by local appends and by the watcher.
|
|
710
729
|
*/
|
|
711
|
-
readonly latest:
|
|
730
|
+
readonly latest: Effect.Effect<Option.Option<EnvelopeUnion<R>>>;
|
|
731
|
+
/** {@link JournalShape.latest} as a stream: the current value, then every change. */
|
|
732
|
+
readonly latestChanges: Stream.Stream<Option.Option<EnvelopeUnion<R>>>;
|
|
712
733
|
/**
|
|
713
734
|
* Whether the journal is quiescent — its tail is an event marked `terminal`.
|
|
714
|
-
*
|
|
715
|
-
* Derived from {@link JournalShape.latest} rather than tracked separately, so
|
|
716
|
-
* the two cannot disagree.
|
|
735
|
+
* Derived from {@link JournalShape.latest}, so the two cannot disagree.
|
|
717
736
|
*/
|
|
718
737
|
readonly quiescent: Effect.Effect<boolean>;
|
|
719
738
|
/**
|
|
720
739
|
* Historical read: a finite `Stream` of the envelopes matching `slice`.
|
|
721
740
|
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
* Filtering happens on the envelope **frame**, strictly before the payload
|
|
727
|
-
* schema runs, so a non-matching line's `data` is never decoded.
|
|
741
|
+
* Resumable across restarts: persist a processed envelope's `position.end`
|
|
742
|
+
* and pass it back as `cursor`. Filtering happens on the envelope frame
|
|
743
|
+
* before the payload schema runs, so a non-matching line's `data` is never
|
|
744
|
+
* decoded.
|
|
728
745
|
*
|
|
729
746
|
* @remarks
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
* reads are `latest` and the `lastValid`-backed paths. Emitting per window,
|
|
737
|
-
* so an unsliced query costs a window rather than a file, is tracked as
|
|
738
|
-
* spencerbeggs/effected#233.
|
|
747
|
+
* The read is paged: the region from `cursor` to the end of the file as of
|
|
748
|
+
* the call is read a bounded page at a time, each page's matches emitted
|
|
749
|
+
* before the next is read. Memory is a page plus the longest line, and a
|
|
750
|
+
* consumer that stops early stops the reading. A line is read once its `\n`
|
|
751
|
+
* lands: an unterminated tail may be a writer mid-append, so it is left out
|
|
752
|
+
* rather than rejected.
|
|
739
753
|
*/
|
|
740
|
-
readonly query: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?:
|
|
754
|
+
readonly query: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?: Slice<R, T> | undefined) => Stream.Stream<EnvelopeWithTag<R, T>, QueryError>;
|
|
741
755
|
/**
|
|
742
|
-
* Live read:
|
|
743
|
-
*
|
|
744
|
-
* With a `cursor`, replay-from-cursor and the live tail are **one seam**: the
|
|
745
|
-
* replayed history and the live tail are the same stream, filtered the same
|
|
746
|
-
* way, so a consumer cannot observe a gap or a duplicate at the join.
|
|
756
|
+
* Live read: matching envelopes as they are appended, by this process or any
|
|
757
|
+
* other writer.
|
|
747
758
|
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
759
|
+
* With a `cursor`, replay and live tail are **one seam**: no gap and no
|
|
760
|
+
* duplicate at the join. The stream **ends** — it never hangs — when the
|
|
761
|
+
* journal becomes quiescent or its scope closes, and fails with
|
|
762
|
+
* `JournalResync` if the file is truncated or replaced beneath it.
|
|
751
763
|
*
|
|
752
764
|
* @remarks
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
* every append that completed is delivered before the stream ends, provided
|
|
757
|
-
* the subscriber keeps taking. One that stops taking cannot observe
|
|
758
|
-
* completion by definition, and shutdown will not wait on it past its bound.
|
|
759
|
-
*
|
|
760
|
-
* The **filter-before-decode guarantee is structural on the historical half
|
|
761
|
-
* only**. Replayed lines are filtered on the frame before their payload is
|
|
762
|
-
* decoded. Live envelopes arrive already decoded — the appender decoded its
|
|
763
|
-
* own payload in order to return it — so a filtered-out live envelope was
|
|
764
|
-
* decoded once, by the writer, not by each subscriber.
|
|
765
|
+
* Every append that completed is delivered before the stream ends, provided
|
|
766
|
+
* the subscriber keeps taking; shutdown will not wait past its bound for one
|
|
767
|
+
* that does not.
|
|
765
768
|
*/
|
|
766
|
-
readonly changes: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?:
|
|
769
|
+
readonly changes: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?: Slice<R, T> | undefined) => Stream.Stream<EnvelopeWithTag<R, T>, ChangesError>;
|
|
767
770
|
/**
|
|
768
|
-
* A running fold over
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
* shared journal is exhaustively checkable against just that scope's events.
|
|
771
|
+
* A running fold over {@link JournalShape.changes}, emitted as it advances.
|
|
772
|
+
* The fold only sees its own slice, so a per-scope state machine is
|
|
773
|
+
* exhaustively checkable against that scope's events alone.
|
|
772
774
|
*/
|
|
773
|
-
readonly projection: <S, T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(initial: S, fold: (state: S, envelope: EnvelopeWithTag<R, T>) => S, slice?:
|
|
774
|
-
/** Create the journal file if it does not exist.
|
|
775
|
+
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>;
|
|
776
|
+
/** Create the journal file if it does not exist. Creation is always explicit. */
|
|
775
777
|
readonly create: Effect.Effect<void, PlatformError.PlatformError>;
|
|
776
|
-
/** Remove the journal file.
|
|
778
|
+
/** Remove the journal file. */
|
|
777
779
|
readonly remove: Effect.Effect<void, PlatformError.PlatformError>;
|
|
778
|
-
/**
|
|
779
|
-
* The internal hub, exposed for the read surfaces built over it.
|
|
780
|
-
*
|
|
781
|
-
* Elements are `Take` chunks and end-of-stream is a published `Exit`, which
|
|
782
|
-
* is what lets quiescence and graceful shutdown arrive at a subscriber as a
|
|
783
|
-
* normal stream end. Never shut this down to signal completion —
|
|
784
|
-
* `PubSub.shutdown` *interrupts* subscribers, which is the tear the design
|
|
785
|
-
* forbids.
|
|
786
|
-
*
|
|
787
|
-
* @internal
|
|
788
|
-
*/
|
|
789
|
-
readonly hub: PubSub.PubSub<Take.Take<EnvelopeUnion<R>, JsonlError>>;
|
|
790
780
|
}
|
|
791
781
|
/**
|
|
792
|
-
*
|
|
782
|
+
* A per-registry `Journal` service class.
|
|
793
783
|
*
|
|
794
784
|
* @public
|
|
795
785
|
*/
|
|
796
|
-
interface
|
|
797
|
-
/**
|
|
798
|
-
readonly
|
|
786
|
+
interface JournalClass<Self, Id extends string, R extends JsonlEvent.Registry, E, RC> extends Context.ServiceClass<Self, Id, JournalShape<R>> {
|
|
787
|
+
/** The registry this journal was defined over. */
|
|
788
|
+
readonly events: R;
|
|
799
789
|
/**
|
|
800
|
-
* The
|
|
790
|
+
* The layer for this journal, built from the definition's `config`.
|
|
801
791
|
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
* package performs, and it is here so the common case needs no second field;
|
|
805
|
-
* name the directory explicitly when your path convention does not survive
|
|
806
|
-
* it — a drive-relative Windows path, or a path whose parent is not a plain
|
|
807
|
-
* prefix of it.
|
|
792
|
+
* One value per class, so providing it twice provides one journal twice —
|
|
793
|
+
* never two unsynchronized journals over the same file.
|
|
808
794
|
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*/
|
|
813
|
-
readonly directory?: string | undefined;
|
|
814
|
-
/**
|
|
815
|
-
* Capacity of the internal subscriber hub. Bounded with backpressure: the
|
|
816
|
-
* journal never drops an envelope for a slow subscriber, so slowness shows
|
|
817
|
-
* up at the producer where it is visible rather than as a silent gap in
|
|
818
|
-
* somebody's subscription.
|
|
795
|
+
* @remarks
|
|
796
|
+
* A journal file that does not exist yet is legal and constructs cleanly; one
|
|
797
|
+
* that exists and cannot be read fails with `PlatformError`.
|
|
819
798
|
*/
|
|
820
|
-
readonly
|
|
799
|
+
readonly layer: Layer.Layer<Self, PlatformError.PlatformError | E, FileSystem.FileSystem | RC>;
|
|
821
800
|
/**
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
* subscriber that never consumes cannot observe completion by definition,
|
|
826
|
-
* and must not be able to hold shutdown hostage.
|
|
801
|
+
* Build a journal over an explicit config, for a path only known at run
|
|
802
|
+
* time. Wrap it in `Layer.effect(Class, Class.make(config))` and bind that
|
|
803
|
+
* layer once: every build is an independent journal.
|
|
827
804
|
*/
|
|
828
|
-
readonly
|
|
805
|
+
readonly make: (config: JournalConfig) => Effect.Effect<JournalShape<R>, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope>;
|
|
829
806
|
}
|
|
830
807
|
/**
|
|
831
|
-
*
|
|
808
|
+
* Defines journal services.
|
|
832
809
|
*
|
|
833
810
|
* @public
|
|
834
811
|
*/
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
readonly events: R;
|
|
812
|
+
export declare class Journal {
|
|
813
|
+
private constructor();
|
|
838
814
|
/**
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
* @remarks
|
|
842
|
-
* **Construction can fail with a `PlatformError`, and that is deliberate.** A
|
|
843
|
-
* journal file that does not exist yet is a legal state and constructs
|
|
844
|
-
* cleanly — but a file that exists and cannot be read (`EACCES`, a bad
|
|
845
|
-
* handle) is a real failure, and typing this channel `never` would have made
|
|
846
|
-
* it arrive as an untypeable defect that no caller could catch.
|
|
815
|
+
* Define a `Journal` service class over a registry.
|
|
847
816
|
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
* of exactly the interleaving the cooperative-writer rules exist to prevent,
|
|
853
|
-
* and it typechecks perfectly.
|
|
817
|
+
* Each registry gets its own uniquely-keyed class, so several journals
|
|
818
|
+
* coexist in one layer graph, each typed by its own registry. `config` is a
|
|
819
|
+
* plain record or an `Effect` producing one — so a path can come from
|
|
820
|
+
* `Config` or another service, resolved when the layer builds.
|
|
854
821
|
*
|
|
822
|
+
* @example
|
|
855
823
|
* ```ts
|
|
856
|
-
*
|
|
857
|
-
*
|
|
824
|
+
* import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
825
|
+
* import { NodeFileSystem } from "@effect/platform-node";
|
|
826
|
+
* import { Effect, Schema } from "effect";
|
|
858
827
|
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
828
|
+
* const events = [JsonlEvent.make("mail", { data: Schema.Struct({ round: Schema.Number }) })] as const;
|
|
829
|
+
*
|
|
830
|
+
* class Mail extends Journal.Service<Mail>()("app/Mail", {
|
|
831
|
+
* events,
|
|
832
|
+
* config: { path: ".app/mail.jsonl" },
|
|
833
|
+
* }) {}
|
|
834
|
+
*
|
|
835
|
+
* const program = Effect.gen(function* () {
|
|
836
|
+
* const mail = yield* Mail;
|
|
837
|
+
* yield* mail.append("mail", { round: 1 });
|
|
838
|
+
* }).pipe(Effect.provide(Mail.layer), Effect.provide(NodeFileSystem.layer));
|
|
861
839
|
* ```
|
|
862
840
|
*/
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Define a `Journal` service class over a registry.
|
|
867
|
-
*
|
|
868
|
-
* A `Context.Service` cannot itself be generic over the registry — the shape
|
|
869
|
-
* binds at declaration and a Key cannot be parameterized at retrieval — so each
|
|
870
|
-
* registry gets its own uniquely-keyed class, and several journals coexist in
|
|
871
|
-
* one layer graph with each one's operations typed by *its* registry. This
|
|
872
|
-
* mirrors `ConfigFile.Service<Self, A>()(id)`.
|
|
873
|
-
*
|
|
874
|
-
* @example
|
|
875
|
-
* ```ts
|
|
876
|
-
* import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
877
|
-
* import { Schema } from "effect";
|
|
878
|
-
*
|
|
879
|
-
* const events = [JsonlEvent.make("started", { data: Schema.Void })] as const;
|
|
880
|
-
*
|
|
881
|
-
* class MailJournal extends Journal.Service<MailJournal>()("dogfood/MailJournal", { events }) {}
|
|
882
|
-
*
|
|
883
|
-
* export const layer = MailJournal.layer({ path: ".claude/dogfood/silk.jsonl" });
|
|
884
|
-
* ```
|
|
885
|
-
*
|
|
886
|
-
* @public
|
|
887
|
-
*/
|
|
888
|
-
export declare const Journal: {
|
|
889
|
-
readonly Service: <Self>() => <const Id extends string, const R extends JsonlEvent.Registry>(id: Id, options: {
|
|
841
|
+
static Service<Self>(): <const Id extends string, const R extends JsonlEvent.Registry, E = never, RC = never>(id: Id, options: {
|
|
890
842
|
readonly events: R;
|
|
891
|
-
|
|
892
|
-
}
|
|
843
|
+
readonly config: JournalConfig | Effect.Effect<JournalConfig, E, RC>;
|
|
844
|
+
}) => JournalClass<Self, Id, R, E, RC>;
|
|
845
|
+
}
|
|
893
846
|
//#endregion
|
|
894
847
|
//#region src/Line.d.ts
|
|
895
|
-
declare const ParsedLine_base: Schema.Class<ParsedLine, Schema.Struct<{
|
|
896
|
-
/** Where this line lives in the source. */
|
|
897
|
-
readonly line: typeof LineSlice;
|
|
898
|
-
/** The parsed JSON value — any JSON value, not necessarily an object. */
|
|
899
|
-
readonly value: Schema.Unknown;
|
|
900
|
-
}>, {}>;
|
|
901
|
-
/**
|
|
902
|
-
* A line that parsed as JSON, paired with the slice it came from.
|
|
903
|
-
*
|
|
904
|
-
* `value` is deliberately `unknown`: this layer knows JSON, not envelopes.
|
|
905
|
-
* Validating `event`, `at`, `scope` and the registered payload schema is the
|
|
906
|
-
* envelope layer's job, and keeping the split means a malformed *envelope* and
|
|
907
|
-
* a malformed *line* stay distinguishable failures.
|
|
908
|
-
*
|
|
909
|
-
* @public
|
|
910
|
-
*/
|
|
911
|
-
export declare class ParsedLine extends ParsedLine_base {}
|
|
912
848
|
/**
|
|
913
|
-
* Splitting
|
|
849
|
+
* Splitting and parsing JSONL text.
|
|
914
850
|
*
|
|
915
|
-
* Every operation is total and synchronous:
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
* in the returned `Result`, because a journal is untrusted input and untrusted
|
|
919
|
-
* input fails typed.
|
|
851
|
+
* Every operation is total and synchronous: nothing here throws. A `JSON.parse`
|
|
852
|
+
* failure becomes a {@link MalformedLine}, because a journal is untrusted input
|
|
853
|
+
* and untrusted input fails typed.
|
|
920
854
|
*
|
|
921
855
|
* @public
|
|
922
856
|
*/
|
|
923
857
|
export declare class Line {
|
|
858
|
+
private constructor();
|
|
924
859
|
/**
|
|
925
860
|
* The UTF-8 byte length of a string.
|
|
926
861
|
*
|
|
927
|
-
* Exposed because callers doing their own offset arithmetic must measure
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* An unpaired surrogate counts as the 3 bytes of U+FFFD, matching what a
|
|
932
|
-
* UTF-8 write actually emits. The offsets therefore stay true to the file
|
|
933
|
-
* even though the character itself cannot survive the round trip — text
|
|
934
|
-
* read back from a UTF-8 journal never contains one.
|
|
862
|
+
* Exposed because callers doing their own offset arithmetic must measure the
|
|
863
|
+
* same way this module does. `String.length` is a different number for any
|
|
864
|
+
* non-ASCII line. An unpaired surrogate counts as the 3 bytes of U+FFFD,
|
|
865
|
+
* matching what a UTF-8 write emits.
|
|
935
866
|
*
|
|
936
867
|
* @example
|
|
937
868
|
* ```ts
|
|
@@ -945,88 +876,31 @@ export declare class Line {
|
|
|
945
876
|
/**
|
|
946
877
|
* Split text into candidate lines with byte-exact offsets.
|
|
947
878
|
*
|
|
948
|
-
* Lines are separated by `\n`; a `\r` immediately preceding it is
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
* a well-formed journal yields exactly as many slices as it has entries.
|
|
954
|
-
* Interior blank lines *are* returned — they are real bytes at real offsets,
|
|
955
|
-
* and hiding them would make the offsets lie.
|
|
956
|
-
*
|
|
957
|
-
* Only the final slice can have `terminated: false`.
|
|
958
|
-
*
|
|
959
|
-
* @param text - JSONL source text.
|
|
960
|
-
* @returns One {@link LineSlice} per candidate line, in source order.
|
|
961
|
-
*/
|
|
962
|
-
static split(text: string): ReadonlyArray<LineSlice>;
|
|
963
|
-
/**
|
|
964
|
-
* The byte offset up to which this text has been fully consumed.
|
|
965
|
-
*
|
|
966
|
-
* This is the offset past the last **terminated** line. An unterminated
|
|
967
|
-
* final line is left unconsumed on purpose: it may be a writer caught
|
|
968
|
-
* mid-append, and re-reading from this offset once the file grows sees the
|
|
969
|
-
* completed line rather than gluing a stale fragment to fresh bytes.
|
|
879
|
+
* Lines are separated by `\n`; a `\r` immediately preceding it is part of the
|
|
880
|
+
* terminator, so CRLF journals split identically to LF ones. A bare `\r` is
|
|
881
|
+
* ordinary content. A trailing terminator does **not** produce a phantom
|
|
882
|
+
* empty final line; interior blank lines *are* returned, because they are
|
|
883
|
+
* real bytes at real offsets. Only the final slice can be unterminated.
|
|
970
884
|
*
|
|
971
885
|
* @param text - JSONL source text.
|
|
972
|
-
* @
|
|
886
|
+
* @param base - The byte offset `text` starts at in its file. Every offset
|
|
887
|
+
* returned is shifted by it, so text read from the middle of a journal
|
|
888
|
+
* yields offsets into the journal rather than into the text.
|
|
889
|
+
* @returns One {@link (LineSlice:type)} per candidate line, in source order.
|
|
973
890
|
*/
|
|
974
|
-
static
|
|
891
|
+
static split(text: string, base?: number): ReadonlyArray<LineSlice>;
|
|
975
892
|
/**
|
|
976
893
|
* Parse one candidate line's JSON.
|
|
977
894
|
*
|
|
978
|
-
* Any JSON value succeeds — objects, arrays and scalars alike.
|
|
979
|
-
* does not know what an envelope is.
|
|
895
|
+
* Any JSON value succeeds — objects, arrays and scalars alike.
|
|
980
896
|
*
|
|
981
897
|
* @param line - A slice from {@link Line.split}.
|
|
982
|
-
* @returns The
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*/
|
|
986
|
-
static parseResult(line: LineSlice): Result.Result<ParsedLine, MalformedLine>;
|
|
987
|
-
/**
|
|
988
|
-
* Parse every non-blank line, reporting failures rather than dropping them.
|
|
989
|
-
*
|
|
990
|
-
* There is deliberately no "valid lines only" variant. A hole in the middle
|
|
991
|
-
* of a journal is information — it means a line was written that no reader
|
|
992
|
-
* can interpret — and whether that is tolerable is the caller's decision,
|
|
993
|
-
* not this module's. Filter the `Result`s yourself and the choice stays
|
|
994
|
-
* visible at the call site.
|
|
995
|
-
*
|
|
996
|
-
* Whitespace-only lines are skipped rather than reported: they carry no
|
|
997
|
-
* information to lose, and reporting them would make a hand-edited journal
|
|
998
|
-
* look corrupt.
|
|
999
|
-
*
|
|
1000
|
-
* @param text - JSONL source text.
|
|
1001
|
-
* @returns One `Result` per non-blank line, in source order.
|
|
1002
|
-
*/
|
|
1003
|
-
static parseAll(text: string): ReadonlyArray<Result.Result<ParsedLine, MalformedLine>>;
|
|
1004
|
-
/**
|
|
1005
|
-
* Walk back from the end to the last line that parses.
|
|
1006
|
-
*
|
|
1007
|
-
* This is the whole read path for a snapshot-style journal, where the
|
|
1008
|
-
* current state *is* the last valid line: a session killed mid-append leaves
|
|
1009
|
-
* a partial final line, and the walk-back steps over it — and over any
|
|
1010
|
-
* number of malformed or blank trailing lines — to the last line that means
|
|
1011
|
-
* something.
|
|
1012
|
-
*
|
|
1013
|
-
* Parsing stops at the first success, so the cost is proportional to the
|
|
1014
|
-
* damage at the tail rather than to the age of the file.
|
|
1015
|
-
*
|
|
1016
|
-
* **A torn tail is only detectable when its fragment fails to parse.**
|
|
1017
|
-
* Every strict prefix of a JSON *object* is invalid JSON, so a truncated
|
|
1018
|
-
* `{"event":"unlinked"}` is always caught. A truncated *scalar* is not:
|
|
1019
|
-
* `42` cut mid-write leaves `4`, which parses cleanly as a different value.
|
|
1020
|
-
* The JSON layer cannot close that hole — the envelope layer can, because
|
|
1021
|
-
* `4` is not an envelope, which is one more reason the envelope is the
|
|
1022
|
-
* package's contract rather than an option. Check `LineSlice.terminated` on
|
|
1023
|
-
* the result when it matters.
|
|
1024
|
-
*
|
|
1025
|
-
* @param text - JSONL source text.
|
|
1026
|
-
* @returns The last parseable line, or `Option.none()` if none parses.
|
|
898
|
+
* @returns The parsed value, or a {@link MalformedLine} carrying the slice so
|
|
899
|
+
* the caller can locate the damage. An unterminated malformed line is a
|
|
900
|
+
* torn tail; a terminated one is a hole that will never heal.
|
|
1027
901
|
*/
|
|
1028
|
-
static
|
|
902
|
+
static parseResult(line: LineSlice): Result.Result<unknown, MalformedLine>;
|
|
1029
903
|
}
|
|
1030
904
|
//#endregion
|
|
1031
|
-
export type { AppendOptions,
|
|
905
|
+
export type { AppendError, AppendOptions, ChangesError, DataSchema, DecodeError, EnvelopeOf, EnvelopeUnion, EnvelopeWithTag, JournalClass, JournalConfig, JournalShape, JsonlError, LinePosition, QueryError, Slice };
|
|
1032
906
|
//# sourceMappingURL=index.d.ts.map
|