@effected/jsonl 0.9.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 +83 -175
- package/Journal.js +42 -466
- package/JsonlError.js +14 -9
- package/JsonlEvent.js +1 -8
- package/Line.js +37 -135
- package/LineSlice.js +12 -10
- package/Slice.js +3 -9
- package/index.d.ts +333 -494
- package/index.js +4 -4
- package/internal/engine.js +399 -0
- package/internal/tail.js +173 -90
- package/package.json +1 -1
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
|
-
*
|
|
429
|
+
* A single candidate line: its text, and where it lives in the source in bytes.
|
|
430
|
+
*
|
|
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.
|
|
446
442
|
*
|
|
447
|
-
*
|
|
448
|
-
|
|
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,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
|
|
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.
|
|
492
503
|
*
|
|
493
504
|
* @example
|
|
494
505
|
* ```ts
|
|
495
|
-
* import { Envelope, JsonlEvent
|
|
496
|
-
* import {
|
|
506
|
+
* import { Envelope, JsonlEvent } from "@effected/jsonl";
|
|
507
|
+
* import { Option, Schema } from "effect";
|
|
497
508
|
*
|
|
498
|
-
* const events = [JsonlEvent.make("
|
|
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
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
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
|
-
*
|
|
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
|
|
535
|
-
* (`MalformedLine`), not an envelope or a bad payload (`InvalidData`), or
|
|
536
|
-
* a tag this registry does not define (`UnknownEvent`).
|
|
529
|
+
* @returns The envelope, or why the line is not one.
|
|
537
530
|
*/
|
|
538
|
-
readonly decodeResult: <const R extends JsonlEvent.Registry>(events: R, line: LineSlice) => Result.Result<EnvelopeUnion<R>,
|
|
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.
|
|
556
|
-
*/
|
|
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
|
-
*
|
|
562
|
-
*
|
|
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>,
|
|
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
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
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
|
|
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
|
|
594
|
-
*
|
|
595
|
-
*
|
|
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
|
|
603
|
-
*
|
|
604
|
-
* @returns The encoded line, or `
|
|
605
|
-
*
|
|
606
|
-
* or
|
|
607
|
-
*
|
|
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,15 @@ export declare const Envelope: {
|
|
|
612
580
|
readonly at: DateTime.Utc;
|
|
613
581
|
readonly scope?: string | undefined;
|
|
614
582
|
}) => Result.Result<string, UnknownEvent | InvalidData | UnserializableData>;
|
|
615
|
-
/**
|
|
616
|
-
* `Envelope.decodeResult` in the `Effect` channel.
|
|
617
|
-
*
|
|
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.
|
|
625
|
-
*
|
|
626
|
-
* Fails with `UnknownEvent`, `InvalidData` or `UnserializableData`; requires
|
|
627
|
-
* nothing.
|
|
628
|
-
*/
|
|
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
583
|
};
|
|
636
584
|
//#endregion
|
|
637
585
|
//#region src/Slice.d.ts
|
|
638
586
|
/**
|
|
639
|
-
* 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.
|
|
640
589
|
*
|
|
641
|
-
* Every field is optional and
|
|
642
|
-
* filter; an empty `events: []` or `scopes: []` matches nothing
|
|
643
|
-
* 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.
|
|
644
592
|
*
|
|
645
593
|
* @public
|
|
646
594
|
*/
|
|
@@ -658,58 +606,98 @@ interface Slice<R extends JsonlEvent.Registry, T extends JsonlEvent.Tag<R> = Jso
|
|
|
658
606
|
/** Lower bound on `at`, **inclusive**. */
|
|
659
607
|
readonly from?: DateTime.Utc | undefined;
|
|
660
608
|
/**
|
|
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`.
|
|
609
|
+
* Upper bound on `at`, **exclusive**, so adjacent windows tile without
|
|
610
|
+
* double-delivering an envelope on the seam.
|
|
666
611
|
*/
|
|
667
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;
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region src/internal/engine.d.ts
|
|
637
|
+
/**
|
|
638
|
+
* Options for one append.
|
|
639
|
+
*
|
|
640
|
+
* @public
|
|
641
|
+
*/
|
|
642
|
+
interface AppendOptions {
|
|
643
|
+
/** The partition key to stamp on the envelope. */
|
|
644
|
+
readonly scope?: string | undefined;
|
|
668
645
|
}
|
|
669
646
|
/**
|
|
670
|
-
*
|
|
647
|
+
* Configuration for one journal.
|
|
671
648
|
*
|
|
672
649
|
* @public
|
|
673
650
|
*/
|
|
674
|
-
interface
|
|
651
|
+
interface JournalConfig {
|
|
652
|
+
/** Path to the journal file. It need not exist yet. */
|
|
653
|
+
readonly path: string;
|
|
675
654
|
/**
|
|
676
|
-
*
|
|
655
|
+
* The directory to watch while the journal does not exist yet.
|
|
677
656
|
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
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.
|
|
682
661
|
*/
|
|
683
|
-
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;
|
|
684
674
|
}
|
|
685
|
-
//#endregion
|
|
686
|
-
//#region src/Journal.d.ts
|
|
687
675
|
/**
|
|
688
|
-
*
|
|
676
|
+
* Why an append failed.
|
|
689
677
|
*
|
|
690
|
-
* `PlatformError`
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
* 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.
|
|
694
681
|
*
|
|
695
682
|
* @public
|
|
696
683
|
*/
|
|
697
|
-
type
|
|
684
|
+
type AppendError = JournalClosed | JournalNotFound | TerminalViolation | InvalidData | UnserializableData | PlatformError.PlatformError;
|
|
698
685
|
/**
|
|
699
|
-
*
|
|
686
|
+
* Why a historical read failed. A {@link DecodeError} only arrives when the
|
|
687
|
+
* slice asks for it with `onInvalid: "fail"`.
|
|
700
688
|
*
|
|
701
689
|
* @public
|
|
702
690
|
*/
|
|
703
|
-
type
|
|
691
|
+
type QueryError = JournalNotFound | DecodeError | PlatformError.PlatformError;
|
|
704
692
|
/**
|
|
705
|
-
*
|
|
693
|
+
* Why a live read failed: anything a query can fail with, plus the file being
|
|
694
|
+
* truncated or replaced beneath the subscriber.
|
|
706
695
|
*
|
|
707
696
|
* @public
|
|
708
697
|
*/
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
698
|
+
type ChangesError = QueryError | JournalResync;
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region src/Journal.d.ts
|
|
713
701
|
/**
|
|
714
702
|
* The shape of a `Journal`, typed by its registry.
|
|
715
703
|
*
|
|
@@ -723,250 +711,158 @@ interface JournalShape<R extends JsonlEvent.Registry> {
|
|
|
723
711
|
* ordering does not depend on two writers agreeing about the time, and
|
|
724
712
|
* `TestClock` controls it exactly in tests.
|
|
725
713
|
*/
|
|
726
|
-
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>;
|
|
727
715
|
/**
|
|
728
|
-
* Inherit-and-patch:
|
|
729
|
-
*
|
|
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.
|
|
730
718
|
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
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.
|
|
734
722
|
*/
|
|
735
|
-
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>;
|
|
736
724
|
/**
|
|
737
|
-
* The current last valid **envelope
|
|
725
|
+
* The journal's current state: its last valid **envelope**.
|
|
738
726
|
*
|
|
739
|
-
* "Last valid"
|
|
740
|
-
*
|
|
741
|
-
* 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.
|
|
742
729
|
*/
|
|
743
|
-
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>>>;
|
|
744
733
|
/**
|
|
745
734
|
* 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.
|
|
735
|
+
* Derived from {@link JournalShape.latest}, so the two cannot disagree.
|
|
749
736
|
*/
|
|
750
737
|
readonly quiescent: Effect.Effect<boolean>;
|
|
751
738
|
/**
|
|
752
739
|
* Historical read: a finite `Stream` of the envelopes matching `slice`.
|
|
753
740
|
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
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.
|
|
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.
|
|
760
745
|
*
|
|
761
746
|
* @remarks
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
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.
|
|
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.
|
|
771
753
|
*/
|
|
772
|
-
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>;
|
|
773
755
|
/**
|
|
774
|
-
* Live read:
|
|
756
|
+
* Live read: matching envelopes as they are appended, by this process or any
|
|
757
|
+
* other writer.
|
|
775
758
|
*
|
|
776
|
-
* With a `cursor`, replay
|
|
777
|
-
*
|
|
778
|
-
*
|
|
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.
|
|
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.
|
|
783
763
|
*
|
|
784
764
|
* @remarks
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
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.
|
|
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.
|
|
797
768
|
*/
|
|
798
|
-
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>;
|
|
799
770
|
/**
|
|
800
|
-
* A running fold over
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* 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.
|
|
804
774
|
*/
|
|
805
|
-
readonly projection: <S, T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(initial: S, fold: (state: S, envelope: EnvelopeWithTag<R, T>) => S, slice?:
|
|
806
|
-
/** 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. */
|
|
807
777
|
readonly create: Effect.Effect<void, PlatformError.PlatformError>;
|
|
808
|
-
/** Remove the journal file.
|
|
778
|
+
/** Remove the journal file. */
|
|
809
779
|
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
780
|
}
|
|
823
781
|
/**
|
|
824
|
-
*
|
|
782
|
+
* A per-registry `Journal` service class.
|
|
825
783
|
*
|
|
826
784
|
* @public
|
|
827
785
|
*/
|
|
828
|
-
interface
|
|
829
|
-
/**
|
|
830
|
-
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;
|
|
831
789
|
/**
|
|
832
|
-
* The
|
|
790
|
+
* The layer for this journal, built from the definition's `config`.
|
|
833
791
|
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
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.
|
|
792
|
+
* One value per class, so providing it twice provides one journal twice —
|
|
793
|
+
* never two unsynchronized journals over the same file.
|
|
840
794
|
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
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.
|
|
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`.
|
|
851
798
|
*/
|
|
852
|
-
readonly
|
|
799
|
+
readonly layer: Layer.Layer<Self, PlatformError.PlatformError | E, FileSystem.FileSystem | RC>;
|
|
853
800
|
/**
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
* subscriber that never consumes cannot observe completion by definition,
|
|
858
|
-
* 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.
|
|
859
804
|
*/
|
|
860
|
-
readonly
|
|
805
|
+
readonly make: (config: JournalConfig) => Effect.Effect<JournalShape<R>, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope>;
|
|
861
806
|
}
|
|
862
807
|
/**
|
|
863
|
-
*
|
|
808
|
+
* Defines journal services.
|
|
864
809
|
*
|
|
865
810
|
* @public
|
|
866
811
|
*/
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
readonly events: R;
|
|
812
|
+
export declare class Journal {
|
|
813
|
+
private constructor();
|
|
870
814
|
/**
|
|
871
|
-
*
|
|
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.
|
|
815
|
+
* Define a `Journal` service class over a registry.
|
|
879
816
|
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
* of exactly the interleaving the cooperative-writer rules exist to prevent,
|
|
885
|
-
* 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.
|
|
886
821
|
*
|
|
822
|
+
* @example
|
|
887
823
|
* ```ts
|
|
888
|
-
*
|
|
889
|
-
*
|
|
824
|
+
* import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
825
|
+
* import { NodeFileSystem } from "@effect/platform-node";
|
|
826
|
+
* import { Effect, Schema } from "effect";
|
|
827
|
+
*
|
|
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
|
+
* }) {}
|
|
890
834
|
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
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));
|
|
893
839
|
* ```
|
|
894
840
|
*/
|
|
895
|
-
|
|
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: {
|
|
841
|
+
static Service<Self>(): <const Id extends string, const R extends JsonlEvent.Registry, E = never, RC = never>(id: Id, options: {
|
|
925
842
|
readonly events: R;
|
|
926
|
-
|
|
927
|
-
}
|
|
843
|
+
readonly config: JournalConfig | Effect.Effect<JournalConfig, E, RC>;
|
|
844
|
+
}) => JournalClass<Self, Id, R, E, RC>;
|
|
845
|
+
}
|
|
928
846
|
//#endregion
|
|
929
847
|
//#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
|
-
/**
|
|
937
|
-
* A line that parsed as JSON, paired with the slice it came from.
|
|
938
|
-
*
|
|
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
848
|
/**
|
|
948
|
-
* Splitting
|
|
849
|
+
* Splitting and parsing JSONL text.
|
|
949
850
|
*
|
|
950
|
-
* Every operation is total and synchronous:
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
* in the returned `Result`, because a journal is untrusted input and untrusted
|
|
954
|
-
* 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.
|
|
955
854
|
*
|
|
956
855
|
* @public
|
|
957
856
|
*/
|
|
958
857
|
export declare class Line {
|
|
858
|
+
private constructor();
|
|
959
859
|
/**
|
|
960
860
|
* The UTF-8 byte length of a string.
|
|
961
861
|
*
|
|
962
|
-
* Exposed because callers doing their own offset arithmetic must measure
|
|
963
|
-
*
|
|
964
|
-
*
|
|
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.
|
|
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.
|
|
970
866
|
*
|
|
971
867
|
* @example
|
|
972
868
|
* ```ts
|
|
@@ -980,88 +876,31 @@ export declare class Line {
|
|
|
980
876
|
/**
|
|
981
877
|
* Split text into candidate lines with byte-exact offsets.
|
|
982
878
|
*
|
|
983
|
-
* Lines are separated by `\n`; a `\r` immediately preceding it is
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
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`.
|
|
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.
|
|
993
884
|
*
|
|
994
885
|
* @param text - JSONL source text.
|
|
995
|
-
* @
|
|
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.
|
|
996
890
|
*/
|
|
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;
|
|
891
|
+
static split(text: string, base?: number): ReadonlyArray<LineSlice>;
|
|
1010
892
|
/**
|
|
1011
893
|
* Parse one candidate line's JSON.
|
|
1012
894
|
*
|
|
1013
|
-
* Any JSON value succeeds — objects, arrays and scalars alike.
|
|
1014
|
-
* does not know what an envelope is.
|
|
895
|
+
* Any JSON value succeeds — objects, arrays and scalars alike.
|
|
1015
896
|
*
|
|
1016
897
|
* @param line - A slice from {@link Line.split}.
|
|
1017
|
-
* @returns The
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
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.
|
|
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.
|
|
1062
901
|
*/
|
|
1063
|
-
static
|
|
902
|
+
static parseResult(line: LineSlice): Result.Result<unknown, MalformedLine>;
|
|
1064
903
|
}
|
|
1065
904
|
//#endregion
|
|
1066
|
-
export type { AppendOptions,
|
|
905
|
+
export type { AppendError, AppendOptions, ChangesError, DataSchema, DecodeError, EnvelopeOf, EnvelopeUnion, EnvelopeWithTag, JournalClass, JournalConfig, JournalShape, JsonlError, LinePosition, QueryError, Slice };
|
|
1067
906
|
//# sourceMappingURL=index.d.ts.map
|