@effected/jsonl 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Envelope.js +83 -175
- package/Journal.js +46 -466
- package/JournalWatcher.js +25 -0
- package/JsonlError.js +14 -9
- package/JsonlEvent.js +1 -8
- package/Line.js +37 -135
- package/LineSlice.js +12 -10
- package/NodeJournalWatcher.js +104 -0
- package/README.md +3 -2
- package/Slice.js +3 -9
- package/index.d.ts +387 -492
- package/index.js +5 -4
- package/internal/engine.js +400 -0
- package/internal/tail.js +173 -90
- package/node.d.ts +37 -0
- package/node.js +3 -0
- package/package.json +6 -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**.
|
|
378
|
+
*
|
|
379
|
+
* Every offset here is a UTF-8 byte offset, never a UTF-16 code-unit index,
|
|
380
|
+
* because these values are cursors into a file that persist across process
|
|
381
|
+
* restarts. A `String.length`-derived offset is correct only for ASCII journals
|
|
382
|
+
* and is the single most likely bug in this module.
|
|
383
|
+
*
|
|
384
|
+
* The terminator is **not** part of the content: `text` and `length` exclude
|
|
385
|
+
* the trailing `\n`, and exclude the `\r` of a `\r\n` pair. `end` includes it,
|
|
386
|
+
* which is why `end - offset` is not always `length`.
|
|
425
387
|
*
|
|
426
|
-
* A
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
* nothing to gain from wrapping each one in a class instance.
|
|
388
|
+
* A plain record rather than a class: `Line.split` produces one per line of
|
|
389
|
+
* every read, and a class instance costs some forty times a plain object on
|
|
390
|
+
* that path. The schema exists so errors can carry the line structurally.
|
|
430
391
|
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
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,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`).
|
|
537
|
-
*/
|
|
538
|
-
readonly decodeResult: <const R extends JsonlEvent.Registry>(events: R, line: LineSlice) => Result.Result<EnvelopeUnion<R>, MalformedLine | InvalidData | UnknownEvent>;
|
|
539
|
-
/**
|
|
540
|
-
* Decode a line only if its **frame** is selected, running the payload schema
|
|
541
|
-
* for selected lines only.
|
|
542
|
-
*
|
|
543
|
-
* This is the structural form of the filter-before-decode guarantee: when
|
|
544
|
-
* `select` rejects the frame, the registered `data` schema is never reached,
|
|
545
|
-
* so a consumer reading a journal it shares with a noisy neighbour pays
|
|
546
|
-
* nothing for the neighbour's payloads. Writing the filter *after* a full
|
|
547
|
-
* decode would produce identical output while charging for every line — which
|
|
548
|
-
* is why this exists as its own operation rather than as advice.
|
|
549
|
-
*
|
|
550
|
-
* @param events - The registry to validate against.
|
|
551
|
-
* @param line - A slice from `Line.split`.
|
|
552
|
-
* @param select - Predicate over the decoded frame — `at`, `event`, `scope`
|
|
553
|
-
* and the raw undecoded `data`.
|
|
554
|
-
* @returns `Option.none()` when the frame is not selected; otherwise the
|
|
555
|
-
* fully decoded envelope, or the typed reason it is not one.
|
|
529
|
+
* @returns The envelope, or why the line is not one.
|
|
556
530
|
*/
|
|
557
|
-
readonly
|
|
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,67 @@ export declare const Envelope: {
|
|
|
612
580
|
readonly at: DateTime.Utc;
|
|
613
581
|
readonly scope?: string | undefined;
|
|
614
582
|
}) => Result.Result<string, UnknownEvent | InvalidData | UnserializableData>;
|
|
583
|
+
};
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/JournalWatcher.d.ts
|
|
586
|
+
/**
|
|
587
|
+
* The service shape behind {@link JournalWatcher}.
|
|
588
|
+
*
|
|
589
|
+
* @public
|
|
590
|
+
*/
|
|
591
|
+
interface JournalWatcherShape {
|
|
615
592
|
/**
|
|
616
|
-
* `
|
|
593
|
+
* Watch `path`, a journal file or the directory it will be created in.
|
|
617
594
|
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
*
|
|
595
|
+
* The effect succeeds only once the watch is **registered**: every change
|
|
596
|
+
* to `path` after it succeeds is delivered on the returned stream. That
|
|
597
|
+
* ordering is the whole contract — the journal arms a watch, then reads
|
|
598
|
+
* what it missed, then follows the stream, and a change landing between a
|
|
599
|
+
* watch that was merely requested and one that was registered would be
|
|
600
|
+
* lost. The watch lives until the enclosing scope closes. Watching a file,
|
|
601
|
+
* the stream ends once the path is removed or names a different file (a
|
|
602
|
+
* rename over it), because a platform watch follows the inode it was armed
|
|
603
|
+
* on and would otherwise go blind; the journal then re-arms on whatever the
|
|
604
|
+
* path names now. Watching a directory, it does not end for a child
|
|
605
|
+
* appearing or vanishing.
|
|
625
606
|
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
607
|
+
* Each element is the name the platform reports for the change, which may
|
|
608
|
+
* be a bare basename and may be absent. The journal treats every element as
|
|
609
|
+
* an untyped poke — "re-stat the file" — and never opens what it names.
|
|
610
|
+
*
|
|
611
|
+
* A path that does not exist fails with a `PlatformError` whose reason is
|
|
612
|
+
* `NotFound`.
|
|
628
613
|
*/
|
|
629
|
-
readonly
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
614
|
+
readonly watch: (path: string) => Effect.Effect<Stream.Stream<string | undefined, PlatformError.PlatformError>, PlatformError.PlatformError, Scope.Scope>;
|
|
615
|
+
}
|
|
616
|
+
declare const JournalWatcher_base: Context.ServiceClass<JournalWatcher, "@effected/jsonl/JournalWatcher", JournalWatcherShape>;
|
|
617
|
+
/**
|
|
618
|
+
* How a journal is told its file changed underneath it: a watch that reports
|
|
619
|
+
* when it is armed.
|
|
620
|
+
*
|
|
621
|
+
* Core's `FileSystem.watch` cannot serve here. Its stream registers the
|
|
622
|
+
* platform watch in a fiber it forks, after an asynchronous `stat`, and gives
|
|
623
|
+
* no signal when that has happened — so a journal reading what it missed
|
|
624
|
+
* before the watch was live cannot know whether a concurrent append landed
|
|
625
|
+
* before registration (and is lost for good) or after (and will be reported).
|
|
626
|
+
* Under load that window is real. This service closes it by construction:
|
|
627
|
+
* {@link JournalWatcherShape.watch} succeeds only once registration is done.
|
|
628
|
+
*
|
|
629
|
+
* The library owns no backend. On Node, provide `NodeJournalWatcher.layer`
|
|
630
|
+
* from `@effected/jsonl/node`; elsewhere, implement the shape over the
|
|
631
|
+
* platform's own watch primitive, honouring the arm-before-success contract.
|
|
632
|
+
*
|
|
633
|
+
* @public
|
|
634
|
+
*/
|
|
635
|
+
export declare class JournalWatcher extends JournalWatcher_base {}
|
|
636
636
|
//#endregion
|
|
637
637
|
//#region src/Slice.d.ts
|
|
638
638
|
/**
|
|
639
|
-
* A filter over envelope fields
|
|
639
|
+
* A filter over envelope fields, plus where to resume and what to do with a
|
|
640
|
+
* line that cannot be decoded.
|
|
640
641
|
*
|
|
641
|
-
* Every field is optional and
|
|
642
|
-
* filter; an empty `events: []` or `scopes: []` matches nothing
|
|
643
|
-
* honest reading of "restrict to this empty set" and is tested.
|
|
642
|
+
* Every field is optional and the filters combine with AND. An omitted field
|
|
643
|
+
* does not filter; an empty `events: []` or `scopes: []` matches nothing.
|
|
644
644
|
*
|
|
645
645
|
* @public
|
|
646
646
|
*/
|
|
@@ -658,58 +658,98 @@ interface Slice<R extends JsonlEvent.Registry, T extends JsonlEvent.Tag<R> = Jso
|
|
|
658
658
|
/** Lower bound on `at`, **inclusive**. */
|
|
659
659
|
readonly from?: DateTime.Utc | undefined;
|
|
660
660
|
/**
|
|
661
|
-
* Upper bound on `at`, **exclusive
|
|
662
|
-
*
|
|
663
|
-
* Half-open so adjacent windows tile without double-delivering an envelope on
|
|
664
|
-
* the seam — which matters because `at` collisions are ordinary at
|
|
665
|
-
* millisecond resolution and universal under `TestClock`.
|
|
661
|
+
* Upper bound on `at`, **exclusive**, so adjacent windows tile without
|
|
662
|
+
* double-delivering an envelope on the seam.
|
|
666
663
|
*/
|
|
667
664
|
readonly to?: DateTime.Utc | undefined;
|
|
665
|
+
/**
|
|
666
|
+
* Resume from this logical byte offset, **inclusive**.
|
|
667
|
+
*
|
|
668
|
+
* Persist a processed envelope's `position.end` and pass it back to replay
|
|
669
|
+
* exactly the remainder. A cursor pointing into a line skips that line whole.
|
|
670
|
+
*/
|
|
671
|
+
readonly cursor?: number | undefined;
|
|
672
|
+
/**
|
|
673
|
+
* What to do with a line that cannot be decoded — not JSON, not an envelope,
|
|
674
|
+
* an unknown tag, or a payload its schema rejects.
|
|
675
|
+
*
|
|
676
|
+
* - `"skip"` (the default): leave it out and keep reading. A journal shared
|
|
677
|
+
* with another writer, or with an older or newer version of this one, stays
|
|
678
|
+
* readable past lines this registry does not understand.
|
|
679
|
+
* - `"fail"`: end the stream with the typed error.
|
|
680
|
+
*
|
|
681
|
+
* A line whose frame decodes but does not match the slice is never decoded
|
|
682
|
+
* further, so it cannot fail a stream that would not have delivered it. One
|
|
683
|
+
* that cannot be framed at all counts against every slice.
|
|
684
|
+
*/
|
|
685
|
+
readonly onInvalid?: "skip" | "fail" | undefined;
|
|
686
|
+
}
|
|
687
|
+
//#endregion
|
|
688
|
+
//#region src/internal/engine.d.ts
|
|
689
|
+
/**
|
|
690
|
+
* Options for one append.
|
|
691
|
+
*
|
|
692
|
+
* @public
|
|
693
|
+
*/
|
|
694
|
+
interface AppendOptions {
|
|
695
|
+
/** The partition key to stamp on the envelope. */
|
|
696
|
+
readonly scope?: string | undefined;
|
|
668
697
|
}
|
|
669
698
|
/**
|
|
670
|
-
*
|
|
699
|
+
* Configuration for one journal.
|
|
671
700
|
*
|
|
672
701
|
* @public
|
|
673
702
|
*/
|
|
674
|
-
interface
|
|
703
|
+
interface JournalConfig {
|
|
704
|
+
/** Path to the journal file. It need not exist yet. */
|
|
705
|
+
readonly path: string;
|
|
675
706
|
/**
|
|
676
|
-
*
|
|
707
|
+
* The directory to watch while the journal does not exist yet.
|
|
677
708
|
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
709
|
+
* Defaults to `path`'s parent — everything before its last `/` or `\`, the
|
|
710
|
+
* one piece of path arithmetic this package performs. Name it explicitly when
|
|
711
|
+
* your path convention does not survive that, such as a drive-relative
|
|
712
|
+
* Windows path. Used only until the journal exists.
|
|
682
713
|
*/
|
|
683
|
-
readonly
|
|
714
|
+
readonly directory?: string | undefined;
|
|
715
|
+
/**
|
|
716
|
+
* Capacity of the internal subscriber hub. Bounded with backpressure: a slow
|
|
717
|
+
* subscriber slows appends rather than silently missing envelopes.
|
|
718
|
+
*/
|
|
719
|
+
readonly capacity?: number | undefined;
|
|
720
|
+
/**
|
|
721
|
+
* How long scope close waits for subscribers to accept the end of the
|
|
722
|
+
* journal. Defaults to five seconds; a subscriber that never consumes must
|
|
723
|
+
* not be able to hold shutdown hostage.
|
|
724
|
+
*/
|
|
725
|
+
readonly shutdownPublishTimeout?: Duration.Input | undefined;
|
|
684
726
|
}
|
|
685
|
-
//#endregion
|
|
686
|
-
//#region src/Journal.d.ts
|
|
687
727
|
/**
|
|
688
|
-
*
|
|
728
|
+
* Why an append failed.
|
|
689
729
|
*
|
|
690
|
-
* `PlatformError`
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
* written". The next reader walks back over whatever fragment survived.
|
|
730
|
+
* `PlatformError` passes through untranslated. **Treat any `PlatformError` from
|
|
731
|
+
* an append as a possibly-torn tail**: a failed write cannot be read as
|
|
732
|
+
* "nothing was written". The next reader walks back over the fragment.
|
|
694
733
|
*
|
|
695
734
|
* @public
|
|
696
735
|
*/
|
|
697
|
-
type
|
|
736
|
+
type AppendError = JournalClosed | JournalNotFound | TerminalViolation | InvalidData | UnserializableData | PlatformError.PlatformError;
|
|
698
737
|
/**
|
|
699
|
-
*
|
|
738
|
+
* Why a historical read failed. A {@link DecodeError} only arrives when the
|
|
739
|
+
* slice asks for it with `onInvalid: "fail"`.
|
|
700
740
|
*
|
|
701
741
|
* @public
|
|
702
742
|
*/
|
|
703
|
-
type
|
|
743
|
+
type QueryError = JournalNotFound | DecodeError | PlatformError.PlatformError;
|
|
704
744
|
/**
|
|
705
|
-
*
|
|
745
|
+
* Why a live read failed: anything a query can fail with, plus the file being
|
|
746
|
+
* truncated or replaced beneath the subscriber.
|
|
706
747
|
*
|
|
707
748
|
* @public
|
|
708
749
|
*/
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
750
|
+
type ChangesError = QueryError | JournalResync;
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region src/Journal.d.ts
|
|
713
753
|
/**
|
|
714
754
|
* The shape of a `Journal`, typed by its registry.
|
|
715
755
|
*
|
|
@@ -723,250 +763,162 @@ interface JournalShape<R extends JsonlEvent.Registry> {
|
|
|
723
763
|
* ordering does not depend on two writers agreeing about the time, and
|
|
724
764
|
* `TestClock` controls it exactly in tests.
|
|
725
765
|
*/
|
|
726
|
-
readonly append: <T extends JsonlEvent.Tag<R>>(event: T, data: JsonlEvent.Data<R, T>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>,
|
|
766
|
+
readonly append: <T extends JsonlEvent.Tag<R>>(event: T, data: JsonlEvent.Data<R, T>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>, AppendError>;
|
|
727
767
|
/**
|
|
728
|
-
* Inherit-and-patch:
|
|
729
|
-
*
|
|
768
|
+
* Inherit-and-patch: shallow-merge `patch` over the current state's `data`,
|
|
769
|
+
* validate the result and append it — read and write under one lock.
|
|
730
770
|
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
771
|
+
* The snapshot-journal primitive: each line is a complete state and most
|
|
772
|
+
* transitions change one field. The merge is **shallow**: a nested object in
|
|
773
|
+
* the patch replaces the one beneath it.
|
|
734
774
|
*/
|
|
735
|
-
readonly appendPatch: <T extends JsonlEvent.Tag<R>>(event: T, patch: Partial<JsonlEvent.Data<R, T>>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>,
|
|
775
|
+
readonly appendPatch: <T extends JsonlEvent.Tag<R>>(event: T, patch: Partial<JsonlEvent.Data<R, T>>, options?: AppendOptions | undefined) => Effect.Effect<EnvelopeWithTag<R, T>, AppendError>;
|
|
736
776
|
/**
|
|
737
|
-
* The current last valid **envelope
|
|
777
|
+
* The journal's current state: its last valid **envelope**.
|
|
738
778
|
*
|
|
739
|
-
* "Last valid"
|
|
740
|
-
*
|
|
741
|
-
* envelope contract detects it.
|
|
779
|
+
* "Last valid" means the last valid envelope, never merely the last valid
|
|
780
|
+
* JSON. Kept current by local appends and by the watcher.
|
|
742
781
|
*/
|
|
743
|
-
readonly latest:
|
|
782
|
+
readonly latest: Effect.Effect<Option.Option<EnvelopeUnion<R>>>;
|
|
783
|
+
/** {@link JournalShape.latest} as a stream: the current value, then every change. */
|
|
784
|
+
readonly latestChanges: Stream.Stream<Option.Option<EnvelopeUnion<R>>>;
|
|
744
785
|
/**
|
|
745
786
|
* Whether the journal is quiescent — its tail is an event marked `terminal`.
|
|
746
|
-
*
|
|
747
|
-
* Derived from {@link JournalShape.latest} rather than tracked separately, so
|
|
748
|
-
* the two cannot disagree.
|
|
787
|
+
* Derived from {@link JournalShape.latest}, so the two cannot disagree.
|
|
749
788
|
*/
|
|
750
789
|
readonly quiescent: Effect.Effect<boolean>;
|
|
751
790
|
/**
|
|
752
791
|
* Historical read: a finite `Stream` of the envelopes matching `slice`.
|
|
753
792
|
*
|
|
754
|
-
*
|
|
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.
|
|
793
|
+
* Resumable across restarts: persist a processed envelope's `position.end`
|
|
794
|
+
* and pass it back as `cursor`. Filtering happens on the envelope frame
|
|
795
|
+
* before the payload schema runs, so a non-matching line's `data` is never
|
|
796
|
+
* decoded.
|
|
760
797
|
*
|
|
761
798
|
* @remarks
|
|
762
|
-
*
|
|
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.
|
|
799
|
+
* The read is paged: the region from `cursor` to the end of the file as of
|
|
800
|
+
* the call is read a bounded page at a time, each page's matches emitted
|
|
801
|
+
* before the next is read. Memory is a page plus the longest line, and a
|
|
802
|
+
* consumer that stops early stops the reading. A line is read once its `\n`
|
|
803
|
+
* lands: an unterminated tail may be a writer mid-append, so it is left out
|
|
804
|
+
* rather than rejected.
|
|
771
805
|
*/
|
|
772
|
-
readonly query: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?:
|
|
806
|
+
readonly query: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?: Slice<R, T> | undefined) => Stream.Stream<EnvelopeWithTag<R, T>, QueryError>;
|
|
773
807
|
/**
|
|
774
|
-
* Live read:
|
|
808
|
+
* Live read: matching envelopes as they are appended, by this process or any
|
|
809
|
+
* other writer.
|
|
775
810
|
*
|
|
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.
|
|
811
|
+
* With a `cursor`, replay and live tail are **one seam**: no gap and no
|
|
812
|
+
* duplicate at the join. The stream **ends** — it never hangs — when the
|
|
813
|
+
* journal becomes quiescent or its scope closes, and fails with
|
|
814
|
+
* `JournalResync` if the file is truncated or replaced beneath it.
|
|
783
815
|
*
|
|
784
816
|
* @remarks
|
|
785
|
-
*
|
|
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.
|
|
817
|
+
* Every append that completed is delivered before the stream ends, provided
|
|
818
|
+
* the subscriber keeps taking; shutdown will not wait past its bound for one
|
|
819
|
+
* that does not.
|
|
797
820
|
*/
|
|
798
|
-
readonly changes: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?:
|
|
821
|
+
readonly changes: <T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(slice?: Slice<R, T> | undefined) => Stream.Stream<EnvelopeWithTag<R, T>, ChangesError>;
|
|
799
822
|
/**
|
|
800
|
-
* A running fold over
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* shared journal is exhaustively checkable against just that scope's events.
|
|
823
|
+
* A running fold over {@link JournalShape.changes}, emitted as it advances.
|
|
824
|
+
* The fold only sees its own slice, so a per-scope state machine is
|
|
825
|
+
* exhaustively checkable against that scope's events alone.
|
|
804
826
|
*/
|
|
805
|
-
readonly projection: <S, T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(initial: S, fold: (state: S, envelope: EnvelopeWithTag<R, T>) => S, slice?:
|
|
806
|
-
/** Create the journal file if it does not exist.
|
|
827
|
+
readonly projection: <S, T extends JsonlEvent.Tag<R> = JsonlEvent.Tag<R>>(initial: S, fold: (state: S, envelope: EnvelopeWithTag<R, T>) => S, slice?: Slice<R, T> | undefined) => Stream.Stream<S, ChangesError>;
|
|
828
|
+
/** Create the journal file if it does not exist. Creation is always explicit. */
|
|
807
829
|
readonly create: Effect.Effect<void, PlatformError.PlatformError>;
|
|
808
|
-
/** Remove the journal file.
|
|
830
|
+
/** Remove the journal file. */
|
|
809
831
|
readonly remove: Effect.Effect<void, PlatformError.PlatformError>;
|
|
810
|
-
/**
|
|
811
|
-
* The internal hub, exposed for the read surfaces built over it.
|
|
812
|
-
*
|
|
813
|
-
* Elements are `Take` chunks and end-of-stream is a published `Exit`, which
|
|
814
|
-
* is what lets quiescence and graceful shutdown arrive at a subscriber as a
|
|
815
|
-
* normal stream end. Never shut this down to signal completion —
|
|
816
|
-
* `PubSub.shutdown` *interrupts* subscribers, which tears an in-flight
|
|
817
|
-
* stream instead of ending it.
|
|
818
|
-
*
|
|
819
|
-
* @internal
|
|
820
|
-
*/
|
|
821
|
-
readonly hub: PubSub.PubSub<Take.Take<EnvelopeUnion<R>, JsonlError>>;
|
|
822
832
|
}
|
|
823
833
|
/**
|
|
824
|
-
*
|
|
834
|
+
* A per-registry `Journal` service class.
|
|
825
835
|
*
|
|
826
836
|
* @public
|
|
827
837
|
*/
|
|
828
|
-
interface
|
|
829
|
-
/**
|
|
830
|
-
readonly
|
|
838
|
+
interface JournalClass<Self, Id extends string, R extends JsonlEvent.Registry, E, RC> extends Context.ServiceClass<Self, Id, JournalShape<R>> {
|
|
839
|
+
/** The registry this journal was defined over. */
|
|
840
|
+
readonly events: R;
|
|
831
841
|
/**
|
|
832
|
-
* The
|
|
842
|
+
* The layer for this journal, built from the definition's `config`.
|
|
833
843
|
*
|
|
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.
|
|
844
|
+
* One value per class, so providing it twice provides one journal twice —
|
|
845
|
+
* never two unsynchronized journals over the same file.
|
|
840
846
|
*
|
|
841
|
-
*
|
|
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.
|
|
847
|
+
* @remarks
|
|
848
|
+
* A journal file that does not exist yet is legal and constructs cleanly; one
|
|
849
|
+
* that exists and cannot be read fails with `PlatformError`.
|
|
851
850
|
*/
|
|
852
|
-
readonly
|
|
851
|
+
readonly layer: Layer.Layer<Self, PlatformError.PlatformError | E, FileSystem.FileSystem | JournalWatcher | RC>;
|
|
853
852
|
/**
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
* subscriber that never consumes cannot observe completion by definition,
|
|
858
|
-
* and must not be able to hold shutdown hostage.
|
|
853
|
+
* Build a journal over an explicit config, for a path only known at run
|
|
854
|
+
* time. Wrap it in `Layer.effect(Class, Class.make(config))` and bind that
|
|
855
|
+
* layer once: every build is an independent journal.
|
|
859
856
|
*/
|
|
860
|
-
readonly
|
|
857
|
+
readonly make: (config: JournalConfig) => Effect.Effect<JournalShape<R>, PlatformError.PlatformError, FileSystem.FileSystem | JournalWatcher | Scope.Scope>;
|
|
861
858
|
}
|
|
862
859
|
/**
|
|
863
|
-
*
|
|
860
|
+
* Defines journal services.
|
|
864
861
|
*
|
|
865
862
|
* @public
|
|
866
863
|
*/
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
readonly events: R;
|
|
864
|
+
export declare class Journal {
|
|
865
|
+
private constructor();
|
|
870
866
|
/**
|
|
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.
|
|
867
|
+
* Define a `Journal` service class over a registry.
|
|
879
868
|
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
* of exactly the interleaving the cooperative-writer rules exist to prevent,
|
|
885
|
-
* and it typechecks perfectly.
|
|
869
|
+
* Each registry gets its own uniquely-keyed class, so several journals
|
|
870
|
+
* coexist in one layer graph, each typed by its own registry. `config` is a
|
|
871
|
+
* plain record or an `Effect` producing one — so a path can come from
|
|
872
|
+
* `Config` or another service, resolved when the layer builds.
|
|
886
873
|
*
|
|
874
|
+
* @example
|
|
887
875
|
* ```ts
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
876
|
+
* import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
877
|
+
* import { NodeJournalWatcher } from "@effected/jsonl/node";
|
|
878
|
+
* import { NodeFileSystem } from "@effect/platform-node";
|
|
879
|
+
* import { Effect, Schema } from "effect";
|
|
880
|
+
*
|
|
881
|
+
* const events = [JsonlEvent.make("mail", { data: Schema.Struct({ round: Schema.Number }) })] as const;
|
|
882
|
+
*
|
|
883
|
+
* class Mail extends Journal.Service<Mail>()("app/Mail", {
|
|
884
|
+
* events,
|
|
885
|
+
* config: { path: ".app/mail.jsonl" },
|
|
886
|
+
* }) {}
|
|
887
|
+
*
|
|
888
|
+
* const program = Effect.gen(function* () {
|
|
889
|
+
* const mail = yield* Mail;
|
|
890
|
+
* yield* mail.append("mail", { round: 1 });
|
|
891
|
+
* }).pipe(
|
|
892
|
+
* Effect.provide(Mail.layer),
|
|
893
|
+
* Effect.provide([NodeFileSystem.layer, NodeJournalWatcher.layer]),
|
|
894
|
+
* );
|
|
893
895
|
* ```
|
|
894
896
|
*/
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* Define a `Journal` service class over a registry.
|
|
899
|
-
*
|
|
900
|
-
* A `Context.Service` cannot itself be generic over the registry — the shape
|
|
901
|
-
* binds at declaration and a Key cannot be parameterized at retrieval — so each
|
|
902
|
-
* registry gets its own uniquely-keyed class, and several journals coexist in
|
|
903
|
-
* one layer graph with each one's operations typed by *its* registry. This
|
|
904
|
-
* mirrors `ConfigFile.Service<Self, A>()(id)`.
|
|
905
|
-
*
|
|
906
|
-
* Pass the registry as `options.events`; the returned class exposes it as
|
|
907
|
-
* `events` and builds the scoped layer with `.layer(config)`.
|
|
908
|
-
*
|
|
909
|
-
* @example
|
|
910
|
-
* ```ts
|
|
911
|
-
* import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
912
|
-
* import { Schema } from "effect";
|
|
913
|
-
*
|
|
914
|
-
* const events = [JsonlEvent.make("started", { data: Schema.Void })] as const;
|
|
915
|
-
*
|
|
916
|
-
* class MailJournal extends Journal.Service<MailJournal>()("app/MailJournal", { events }) {}
|
|
917
|
-
*
|
|
918
|
-
* export const layer = MailJournal.layer({ path: ".app/mail.jsonl" });
|
|
919
|
-
* ```
|
|
920
|
-
*
|
|
921
|
-
* @public
|
|
922
|
-
*/
|
|
923
|
-
export declare const Journal: {
|
|
924
|
-
readonly Service: <Self>() => <const Id extends string, const R extends JsonlEvent.Registry>(id: Id, options: {
|
|
897
|
+
static Service<Self>(): <const Id extends string, const R extends JsonlEvent.Registry, E = never, RC = never>(id: Id, options: {
|
|
925
898
|
readonly events: R;
|
|
926
|
-
|
|
927
|
-
}
|
|
899
|
+
readonly config: JournalConfig | Effect.Effect<JournalConfig, E, RC>;
|
|
900
|
+
}) => JournalClass<Self, Id, R, E, RC>;
|
|
901
|
+
}
|
|
928
902
|
//#endregion
|
|
929
903
|
//#region src/Line.d.ts
|
|
930
|
-
declare const ParsedLine_base: Schema.Class<ParsedLine, Schema.Struct<{
|
|
931
|
-
/** Where this line lives in the source. */
|
|
932
|
-
readonly line: typeof LineSlice;
|
|
933
|
-
/** The parsed JSON value — any JSON value, not necessarily an object. */
|
|
934
|
-
readonly value: Schema.Unknown;
|
|
935
|
-
}>, {}>;
|
|
936
904
|
/**
|
|
937
|
-
*
|
|
905
|
+
* Splitting and parsing JSONL text.
|
|
938
906
|
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
* a malformed *line* stay distinguishable failures.
|
|
943
|
-
*
|
|
944
|
-
* @public
|
|
945
|
-
*/
|
|
946
|
-
export declare class ParsedLine extends ParsedLine_base {}
|
|
947
|
-
/**
|
|
948
|
-
* Splitting, parsing and corrupt-tail walk-back over JSONL text.
|
|
949
|
-
*
|
|
950
|
-
* Every operation is total and synchronous: it returns a value for every
|
|
951
|
-
* input, including empty text, torn tails, and text that is not JSONL at all.
|
|
952
|
-
* Nothing here throws — a `JSON.parse` failure becomes a {@link MalformedLine}
|
|
953
|
-
* in the returned `Result`, because a journal is untrusted input and untrusted
|
|
954
|
-
* input fails typed.
|
|
907
|
+
* Every operation is total and synchronous: nothing here throws. A `JSON.parse`
|
|
908
|
+
* failure becomes a {@link MalformedLine}, because a journal is untrusted input
|
|
909
|
+
* and untrusted input fails typed.
|
|
955
910
|
*
|
|
956
911
|
* @public
|
|
957
912
|
*/
|
|
958
913
|
export declare class Line {
|
|
914
|
+
private constructor();
|
|
959
915
|
/**
|
|
960
916
|
* The UTF-8 byte length of a string.
|
|
961
917
|
*
|
|
962
|
-
* Exposed because callers doing their own offset arithmetic must measure
|
|
963
|
-
*
|
|
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.
|
|
918
|
+
* Exposed because callers doing their own offset arithmetic must measure the
|
|
919
|
+
* same way this module does. `String.length` is a different number for any
|
|
920
|
+
* non-ASCII line. An unpaired surrogate counts as the 3 bytes of U+FFFD,
|
|
921
|
+
* matching what a UTF-8 write emits.
|
|
970
922
|
*
|
|
971
923
|
* @example
|
|
972
924
|
* ```ts
|
|
@@ -980,88 +932,31 @@ export declare class Line {
|
|
|
980
932
|
/**
|
|
981
933
|
* Split text into candidate lines with byte-exact offsets.
|
|
982
934
|
*
|
|
983
|
-
* Lines are separated by `\n`; a `\r` immediately preceding it is
|
|
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`.
|
|
935
|
+
* Lines are separated by `\n`; a `\r` immediately preceding it is part of the
|
|
936
|
+
* terminator, so CRLF journals split identically to LF ones. A bare `\r` is
|
|
937
|
+
* ordinary content. A trailing terminator does **not** produce a phantom
|
|
938
|
+
* empty final line; interior blank lines *are* returned, because they are
|
|
939
|
+
* real bytes at real offsets. Only the final slice can be unterminated.
|
|
993
940
|
*
|
|
994
941
|
* @param text - JSONL source text.
|
|
995
|
-
* @
|
|
942
|
+
* @param base - The byte offset `text` starts at in its file. Every offset
|
|
943
|
+
* returned is shifted by it, so text read from the middle of a journal
|
|
944
|
+
* yields offsets into the journal rather than into the text.
|
|
945
|
+
* @returns One {@link (LineSlice:type)} per candidate line, in source order.
|
|
996
946
|
*/
|
|
997
|
-
static split(text: string): ReadonlyArray<LineSlice>;
|
|
998
|
-
/**
|
|
999
|
-
* The byte offset up to which this text has been fully consumed.
|
|
1000
|
-
*
|
|
1001
|
-
* This is the offset past the last **terminated** line. An unterminated
|
|
1002
|
-
* final line is left unconsumed on purpose: it may be a writer caught
|
|
1003
|
-
* mid-append, and re-reading from this offset once the file grows sees the
|
|
1004
|
-
* completed line rather than gluing a stale fragment to fresh bytes.
|
|
1005
|
-
*
|
|
1006
|
-
* @param text - JSONL source text.
|
|
1007
|
-
* @returns The resume cursor, in UTF-8 bytes.
|
|
1008
|
-
*/
|
|
1009
|
-
static consumedOffset(text: string): number;
|
|
947
|
+
static split(text: string, base?: number): ReadonlyArray<LineSlice>;
|
|
1010
948
|
/**
|
|
1011
949
|
* Parse one candidate line's JSON.
|
|
1012
950
|
*
|
|
1013
|
-
* Any JSON value succeeds — objects, arrays and scalars alike.
|
|
1014
|
-
* does not know what an envelope is.
|
|
951
|
+
* Any JSON value succeeds — objects, arrays and scalars alike.
|
|
1015
952
|
*
|
|
1016
953
|
* @param line - A slice from {@link Line.split}.
|
|
1017
|
-
* @returns The
|
|
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.
|
|
954
|
+
* @returns The parsed value, or a {@link MalformedLine} carrying the slice so
|
|
955
|
+
* the caller can locate the damage. An unterminated malformed line is a
|
|
956
|
+
* torn tail; a terminated one is a hole that will never heal.
|
|
1062
957
|
*/
|
|
1063
|
-
static
|
|
958
|
+
static parseResult(line: LineSlice): Result.Result<unknown, MalformedLine>;
|
|
1064
959
|
}
|
|
1065
960
|
//#endregion
|
|
1066
|
-
export type { AppendOptions,
|
|
961
|
+
export type { AppendError, AppendOptions, ChangesError, DataSchema, DecodeError, EnvelopeOf, EnvelopeUnion, EnvelopeWithTag, JournalClass, JournalConfig, JournalShape, JournalWatcherShape, JsonlError, LinePosition, QueryError, Slice };
|
|
1067
962
|
//# sourceMappingURL=index.d.ts.map
|