@effected/jsonl 0.10.0 → 0.11.1
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/Journal.js +5 -1
- package/JournalWatcher.js +25 -0
- package/NodeJournalWatcher.js +104 -0
- package/README.md +3 -2
- package/index.d.ts +60 -4
- package/index.js +2 -1
- package/internal/engine.js +17 -16
- package/node.d.ts +37 -0
- package/node.js +3 -0
- package/package.json +7 -2
package/Journal.js
CHANGED
|
@@ -20,6 +20,7 @@ var Journal = class {
|
|
|
20
20
|
* @example
|
|
21
21
|
* ```ts
|
|
22
22
|
* import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
23
|
+
* import { NodeJournalWatcher } from "@effected/jsonl/node";
|
|
23
24
|
* import { NodeFileSystem } from "@effect/platform-node";
|
|
24
25
|
* import { Effect, Schema } from "effect";
|
|
25
26
|
*
|
|
@@ -33,7 +34,10 @@ var Journal = class {
|
|
|
33
34
|
* const program = Effect.gen(function* () {
|
|
34
35
|
* const mail = yield* Mail;
|
|
35
36
|
* yield* mail.append("mail", { round: 1 });
|
|
36
|
-
* }).pipe(
|
|
37
|
+
* }).pipe(
|
|
38
|
+
* Effect.provide(Mail.layer),
|
|
39
|
+
* Effect.provide([NodeFileSystem.layer, NodeJournalWatcher.layer]),
|
|
40
|
+
* );
|
|
37
41
|
* ```
|
|
38
42
|
*/
|
|
39
43
|
static Service() {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/JournalWatcher.ts
|
|
4
|
+
/**
|
|
5
|
+
* How a journal is told its file changed underneath it: a watch that reports
|
|
6
|
+
* when it is armed.
|
|
7
|
+
*
|
|
8
|
+
* Core's `FileSystem.watch` cannot serve here. Its stream registers the
|
|
9
|
+
* platform watch in a fiber it forks, after an asynchronous `stat`, and gives
|
|
10
|
+
* no signal when that has happened — so a journal reading what it missed
|
|
11
|
+
* before the watch was live cannot know whether a concurrent append landed
|
|
12
|
+
* before registration (and is lost for good) or after (and will be reported).
|
|
13
|
+
* Under load that window is real. This service closes it by construction:
|
|
14
|
+
* {@link JournalWatcherShape.watch} succeeds only once registration is done.
|
|
15
|
+
*
|
|
16
|
+
* The library owns no backend. On Node, provide `NodeJournalWatcher.layer`
|
|
17
|
+
* from `@effected/jsonl/node`; elsewhere, implement the shape over the
|
|
18
|
+
* platform's own watch primitive, honouring the arm-before-success contract.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
var JournalWatcher = class extends Context.Service()("@effected/jsonl/JournalWatcher") {};
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { JournalWatcher };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { JournalWatcher } from "./JournalWatcher.js";
|
|
2
|
+
import { Cause, Effect, Layer, PlatformError, Queue, Stream } from "effect";
|
|
3
|
+
import * as NFS from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region src/NodeJournalWatcher.ts
|
|
6
|
+
/** The `SystemErrorTag` an errno code maps to, after core's Node filesystem mapping. */
|
|
7
|
+
const reasonOf = (code) => {
|
|
8
|
+
switch (code) {
|
|
9
|
+
case "ENOENT": return "NotFound";
|
|
10
|
+
case "EACCES":
|
|
11
|
+
case "EPERM": return "PermissionDenied";
|
|
12
|
+
case "ENOTDIR":
|
|
13
|
+
case "ELOOP": return "BadResource";
|
|
14
|
+
default: return "Unknown";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const watchError = (path, cause) => PlatformError.systemError({
|
|
18
|
+
_tag: reasonOf(cause?.code),
|
|
19
|
+
module: "FileSystem",
|
|
20
|
+
method: "watch",
|
|
21
|
+
pathOrDescriptor: path,
|
|
22
|
+
syscall: "watch",
|
|
23
|
+
cause
|
|
24
|
+
});
|
|
25
|
+
/** The identity a watched file is armed on, or `undefined` once the path names nothing. */
|
|
26
|
+
const identityAt = (path) => {
|
|
27
|
+
try {
|
|
28
|
+
const info = NFS.statSync(path);
|
|
29
|
+
return `${info.dev}:${info.ino}`;
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Register `fs.watch` synchronously, inside the acquire — so when the effect
|
|
36
|
+
* succeeds the platform watch is live, which is the contract
|
|
37
|
+
* {@link Jsonl.JournalWatcherShape.watch} states and core's
|
|
38
|
+
* `FileSystem.watch` cannot.
|
|
39
|
+
*
|
|
40
|
+
* A file watch follows the inode it was armed on, and Node emits neither
|
|
41
|
+
* `close` nor `error` when that inode is unlinked or renamed over: left
|
|
42
|
+
* alone, the stream would outlive the file and the journal would follow a
|
|
43
|
+
* dead inode forever. So every event on a file re-checks the path's identity,
|
|
44
|
+
* and the stream ends — after delivering the event — once the path is gone or
|
|
45
|
+
* names a different file. A directory watch never ends that way: a sibling
|
|
46
|
+
* appearing or vanishing is routine there.
|
|
47
|
+
*/
|
|
48
|
+
const watch = (path) => Effect.gen(function* () {
|
|
49
|
+
const queue = yield* Queue.unbounded();
|
|
50
|
+
yield* Effect.acquireRelease(Effect.try({
|
|
51
|
+
try: () => {
|
|
52
|
+
const armed = NFS.statSync(path);
|
|
53
|
+
const identity = armed.isDirectory() ? void 0 : `${armed.dev}:${armed.ino}`;
|
|
54
|
+
const watcher = NFS.watch(path, (_event, name) => {
|
|
55
|
+
Queue.offerUnsafe(queue, name ?? void 0);
|
|
56
|
+
if (identity !== void 0 && identityAt(path) !== identity) Queue.endUnsafe(queue);
|
|
57
|
+
});
|
|
58
|
+
watcher.on("error", (cause) => {
|
|
59
|
+
Queue.failCauseUnsafe(queue, Cause.fail(watchError(path, cause)));
|
|
60
|
+
});
|
|
61
|
+
watcher.on("close", () => {
|
|
62
|
+
Queue.endUnsafe(queue);
|
|
63
|
+
});
|
|
64
|
+
return watcher;
|
|
65
|
+
},
|
|
66
|
+
catch: (cause) => watchError(path, cause)
|
|
67
|
+
}), (watcher) => Effect.sync(() => watcher.close()));
|
|
68
|
+
return Stream.fromQueue(queue);
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* The Node implementation of {@link Jsonl.JournalWatcher}, over `node:fs`'s
|
|
72
|
+
* `watch`.
|
|
73
|
+
*
|
|
74
|
+
* Registration happens synchronously while the watch effect runs, so a
|
|
75
|
+
* journal's catch-up read after arming can never miss an append that the
|
|
76
|
+
* watch also fails to report.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
81
|
+
* import { NodeJournalWatcher } from "@effected/jsonl/node";
|
|
82
|
+
* import { NodeFileSystem } from "@effect/platform-node";
|
|
83
|
+
* import { Effect, Schema } from "effect";
|
|
84
|
+
*
|
|
85
|
+
* const events = [JsonlEvent.make("mail", { data: Schema.Struct({ round: Schema.Number }) })] as const;
|
|
86
|
+
*
|
|
87
|
+
* class Mail extends Journal.Service<Mail>()("app/Mail", { events, config: { path: ".app/mail.jsonl" } }) {}
|
|
88
|
+
*
|
|
89
|
+
* const program = Effect.gen(function* () {
|
|
90
|
+
* const mail = yield* Mail;
|
|
91
|
+
* yield* mail.append("mail", { round: 1 });
|
|
92
|
+
* }).pipe(Effect.provide(Mail.layer), Effect.provide([NodeFileSystem.layer, NodeJournalWatcher.layer]));
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @public
|
|
96
|
+
*/
|
|
97
|
+
var NodeJournalWatcher = class {
|
|
98
|
+
constructor() {}
|
|
99
|
+
/** Provides {@link Jsonl.JournalWatcher} over `node:fs`. */
|
|
100
|
+
static layer = Layer.succeed(JournalWatcher, { watch });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { NodeJournalWatcher };
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Requires Node.js >=24.11.0. `effect` v4 is the only peer dependency, and the onl
|
|
|
41
41
|
|
|
42
42
|
All `@effected/*` packages are ESM-only: the exports maps publish only `import` conditions, so `require()` — including tools that resolve in CJS mode — fails with Node's `ERR_PACKAGE_PATH_NOT_EXPORTED` rather than loading a CJS build that does not exist. Import from an ES module.
|
|
43
43
|
|
|
44
|
-
`FileSystem` comes from `effect` core, not from a platform package, so a consumer provides it once at the edge (`NodeFileSystem.layer` from `@effect/platform-node` on Node). `Path` is deliberately not required — every path is an opaque string handed straight to `FileSystem`, and this package never joins, resolves or splits one.
|
|
44
|
+
`FileSystem` comes from `effect` core, not from a platform package, so a consumer provides it once at the edge (`NodeFileSystem.layer` from `@effect/platform-node` on Node). The journal also needs a `JournalWatcher` — a file watch that reports when it is armed, so an append cannot land unseen while the watch is being set up; on Node that is `NodeJournalWatcher.layer` from the `@effected/jsonl/node` subpath, the only part of the package that touches `node:*`. `Path` is deliberately not required — every path is an opaque string handed straight to `FileSystem`, and this package never joins, resolves or splits one.
|
|
45
45
|
|
|
46
46
|
## Quick start
|
|
47
47
|
|
|
@@ -49,6 +49,7 @@ Declare the events a journal may contain, mint a service class for that registry
|
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
51
|
import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
52
|
+
import { NodeJournalWatcher } from "@effected/jsonl/node";
|
|
52
53
|
import { NodeFileSystem } from "@effect/platform-node";
|
|
53
54
|
import { Effect, Option, Schema, Stream, SubscriptionRef } from "effect";
|
|
54
55
|
|
|
@@ -90,7 +91,7 @@ const program = Effect.gen(function* () {
|
|
|
90
91
|
);
|
|
91
92
|
});
|
|
92
93
|
|
|
93
|
-
Effect.runPromise(program.pipe(Effect.provide(MailJournalLive), Effect.provide(NodeFileSystem.layer)));
|
|
94
|
+
Effect.runPromise(program.pipe(Effect.provide(MailJournalLive), Effect.provide([NodeFileSystem.layer, NodeJournalWatcher.layer])));
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
`events: ["mail-received"]` narrows the stream's element type to that variant, so a projection over a slice is exhaustively checkable rather than a cast. An event marked `terminal` makes the journal quiescent once it reaches the tail: further appends fail typed unless the event is marked `reopen`, and every `changes` stream ends rather than hanging — including one whose slice excludes the terminal event, and including a subscriber that attaches after the fact.
|
package/index.d.ts
CHANGED
|
@@ -582,6 +582,58 @@ export declare const Envelope: {
|
|
|
582
582
|
}) => Result.Result<string, UnknownEvent | InvalidData | UnserializableData>;
|
|
583
583
|
};
|
|
584
584
|
//#endregion
|
|
585
|
+
//#region src/JournalWatcher.d.ts
|
|
586
|
+
/**
|
|
587
|
+
* The service shape behind {@link JournalWatcher}.
|
|
588
|
+
*
|
|
589
|
+
* @public
|
|
590
|
+
*/
|
|
591
|
+
interface JournalWatcherShape {
|
|
592
|
+
/**
|
|
593
|
+
* Watch `path`, a journal file or the directory it will be created in.
|
|
594
|
+
*
|
|
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.
|
|
606
|
+
*
|
|
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`.
|
|
613
|
+
*/
|
|
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
|
+
//#endregion
|
|
585
637
|
//#region src/Slice.d.ts
|
|
586
638
|
/**
|
|
587
639
|
* A filter over envelope fields, plus where to resume and what to do with a
|
|
@@ -796,13 +848,13 @@ interface JournalClass<Self, Id extends string, R extends JsonlEvent.Registry, E
|
|
|
796
848
|
* A journal file that does not exist yet is legal and constructs cleanly; one
|
|
797
849
|
* that exists and cannot be read fails with `PlatformError`.
|
|
798
850
|
*/
|
|
799
|
-
readonly layer: Layer.Layer<Self, PlatformError.PlatformError | E, FileSystem.FileSystem | RC>;
|
|
851
|
+
readonly layer: Layer.Layer<Self, PlatformError.PlatformError | E, FileSystem.FileSystem | JournalWatcher | RC>;
|
|
800
852
|
/**
|
|
801
853
|
* Build a journal over an explicit config, for a path only known at run
|
|
802
854
|
* time. Wrap it in `Layer.effect(Class, Class.make(config))` and bind that
|
|
803
855
|
* layer once: every build is an independent journal.
|
|
804
856
|
*/
|
|
805
|
-
readonly make: (config: JournalConfig) => Effect.Effect<JournalShape<R>, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope>;
|
|
857
|
+
readonly make: (config: JournalConfig) => Effect.Effect<JournalShape<R>, PlatformError.PlatformError, FileSystem.FileSystem | JournalWatcher | Scope.Scope>;
|
|
806
858
|
}
|
|
807
859
|
/**
|
|
808
860
|
* Defines journal services.
|
|
@@ -822,6 +874,7 @@ export declare class Journal {
|
|
|
822
874
|
* @example
|
|
823
875
|
* ```ts
|
|
824
876
|
* import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
877
|
+
* import { NodeJournalWatcher } from "@effected/jsonl/node";
|
|
825
878
|
* import { NodeFileSystem } from "@effect/platform-node";
|
|
826
879
|
* import { Effect, Schema } from "effect";
|
|
827
880
|
*
|
|
@@ -835,7 +888,10 @@ export declare class Journal {
|
|
|
835
888
|
* const program = Effect.gen(function* () {
|
|
836
889
|
* const mail = yield* Mail;
|
|
837
890
|
* yield* mail.append("mail", { round: 1 });
|
|
838
|
-
* }).pipe(
|
|
891
|
+
* }).pipe(
|
|
892
|
+
* Effect.provide(Mail.layer),
|
|
893
|
+
* Effect.provide([NodeFileSystem.layer, NodeJournalWatcher.layer]),
|
|
894
|
+
* );
|
|
839
895
|
* ```
|
|
840
896
|
*/
|
|
841
897
|
static Service<Self>(): <const Id extends string, const R extends JsonlEvent.Registry, E = never, RC = never>(id: Id, options: {
|
|
@@ -902,5 +958,5 @@ export declare class Line {
|
|
|
902
958
|
static parseResult(line: LineSlice): Result.Result<unknown, MalformedLine>;
|
|
903
959
|
}
|
|
904
960
|
//#endregion
|
|
905
|
-
export type { AppendError, AppendOptions, ChangesError, DataSchema, DecodeError, EnvelopeOf, EnvelopeUnion, EnvelopeWithTag, JournalClass, JournalConfig, JournalShape, JsonlError, LinePosition, QueryError, Slice };
|
|
961
|
+
export type { AppendError, AppendOptions, ChangesError, DataSchema, DecodeError, EnvelopeOf, EnvelopeUnion, EnvelopeWithTag, JournalClass, JournalConfig, JournalShape, JournalWatcherShape, JsonlError, LinePosition, QueryError, Slice };
|
|
906
962
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { LineSlice } from "./LineSlice.js";
|
|
|
2
2
|
import { InvalidData, JournalClosed, JournalNotFound, JournalResync, MalformedLine, TerminalViolation, UnknownEvent, UnserializableData } from "./JsonlError.js";
|
|
3
3
|
import { Line } from "./Line.js";
|
|
4
4
|
import { Envelope } from "./Envelope.js";
|
|
5
|
+
import { JournalWatcher } from "./JournalWatcher.js";
|
|
5
6
|
import { Journal } from "./Journal.js";
|
|
6
7
|
import { JsonlEvent } from "./JsonlEvent.js";
|
|
7
8
|
|
|
8
|
-
export { Envelope, InvalidData, Journal, JournalClosed, JournalNotFound, JournalResync, JsonlEvent, Line, LineSlice, MalformedLine, TerminalViolation, UnknownEvent, UnserializableData };
|
|
9
|
+
export { Envelope, InvalidData, Journal, JournalClosed, JournalNotFound, JournalResync, JournalWatcher, JsonlEvent, Line, LineSlice, MalformedLine, TerminalViolation, UnknownEvent, UnserializableData };
|
package/internal/engine.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { JournalClosed, JournalNotFound, JournalResync, TerminalViolation } from "../JsonlError.js";
|
|
2
2
|
import { Line, isBlank } from "../Line.js";
|
|
3
3
|
import { Envelope, completeResult, frameResult } from "../Envelope.js";
|
|
4
|
+
import { JournalWatcher } from "../JournalWatcher.js";
|
|
4
5
|
import { matchesFrame } from "../Slice.js";
|
|
5
6
|
import { canMerge, shallowMerge } from "./merge.js";
|
|
6
7
|
import { probeBomBytes, readLinePages, readTailUntil } from "./tail.js";
|
|
7
|
-
import { ByteSize, Channel, DateTime, Deferred, Duration, Effect, Exit,
|
|
8
|
+
import { ByteSize, Channel, DateTime, Deferred, Duration, Effect, Exit, FileSystem, Option, PubSub, Result, Semaphore, Stream, SubscriptionRef } from "effect";
|
|
8
9
|
|
|
9
10
|
//#region src/internal/engine.ts
|
|
10
11
|
const positionOf = (item) => Result.isSuccess(item) ? item.success.position : item.failure.position;
|
|
@@ -16,14 +17,6 @@ const SHUTDOWN_PUBLISH_TIMEOUT = Duration.seconds(5);
|
|
|
16
17
|
* keep working, live observation does not.
|
|
17
18
|
*/
|
|
18
19
|
const MAX_IMMEDIATE_REARMS = 8;
|
|
19
|
-
/**
|
|
20
|
-
* Scheduler turns yielded to a freshly-forked watch consumer before the
|
|
21
|
-
* catch-up read. `fs.watch` exposes no "registered" signal, so arming is
|
|
22
|
-
* ordered ahead of catch-up by scheduling, not synchronisation — empirically
|
|
23
|
-
* sufficient, not a proof. The airtight primitive would be
|
|
24
|
-
* `FileSystem.WatchBackend.register`, at the cost of `WatchBackend` in `R`.
|
|
25
|
-
*/
|
|
26
|
-
const ARM_YIELDS = 3;
|
|
27
20
|
const encoder = new TextEncoder();
|
|
28
21
|
/**
|
|
29
22
|
* Index of the last path separator, on either convention — a Windows path
|
|
@@ -64,6 +57,7 @@ const admit = (items, slice) => {
|
|
|
64
57
|
*/
|
|
65
58
|
const makeEngine = (id, events, config) => Effect.gen(function* () {
|
|
66
59
|
const fs = yield* FileSystem.FileSystem;
|
|
60
|
+
const watcher = yield* JournalWatcher;
|
|
67
61
|
const { path } = config;
|
|
68
62
|
const terminalTags = new Set(events.filter((event) => event.terminal).map((event) => event.tag));
|
|
69
63
|
const reopenTags = new Set(events.filter((event) => event.reopen).map((event) => event.tag));
|
|
@@ -366,19 +360,26 @@ const makeEngine = (id, events, config) => Effect.gen(function* () {
|
|
|
366
360
|
if (Option.isSome(initial)) yield* seed(initial.value).pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.void));
|
|
367
361
|
const basename = basenameOf(path);
|
|
368
362
|
const directory = config.directory ?? parentOf(path);
|
|
369
|
-
|
|
370
|
-
const
|
|
363
|
+
/** A directory event names the journal, or names nothing and might. */
|
|
364
|
+
const namesJournal = (name) => name === void 0 || basenameOf(name) === basename;
|
|
365
|
+
const followJournal = Effect.scoped(Effect.gen(function* () {
|
|
366
|
+
const changes = yield* watcher.watch(path);
|
|
367
|
+
yield* ingest;
|
|
368
|
+
yield* Stream.runForEach(changes, () => ingest);
|
|
369
|
+
}));
|
|
370
|
+
const awaitCreation = Effect.scoped(Effect.gen(function* () {
|
|
371
|
+
const changes = yield* watcher.watch(directory);
|
|
372
|
+
if (yield* fs.exists(path)) return;
|
|
373
|
+
yield* changes.pipe(Stream.filter(namesJournal), Stream.takeUntilEffect(() => fs.exists(path)), Stream.runForEach(() => ingest));
|
|
374
|
+
}));
|
|
371
375
|
const supervise = Effect.gen(function* () {
|
|
372
376
|
let immediateCompletions = 0;
|
|
373
377
|
for (;;) {
|
|
374
378
|
const startedAt = consumed;
|
|
375
379
|
if (yield* fs.exists(path)) {
|
|
376
|
-
|
|
377
|
-
for (let turn = 0; turn < ARM_YIELDS; turn++) yield* Effect.yieldNow;
|
|
378
|
-
yield* ingest;
|
|
379
|
-
yield* Fiber.join(armed);
|
|
380
|
+
yield* Effect.ignore(followJournal);
|
|
380
381
|
yield* ingest;
|
|
381
|
-
} else yield*
|
|
382
|
+
} else yield* Effect.ignore(awaitCreation);
|
|
382
383
|
immediateCompletions = consumed === startedAt ? immediateCompletions + 1 : 0;
|
|
383
384
|
if (immediateCompletions > MAX_IMMEDIATE_REARMS) return;
|
|
384
385
|
}
|
package/node.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Jsonl from "@effected/jsonl";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
//#region src/NodeJournalWatcher.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The Node implementation of {@link Jsonl.JournalWatcher}, over `node:fs`'s
|
|
6
|
+
* `watch`.
|
|
7
|
+
*
|
|
8
|
+
* Registration happens synchronously while the watch effect runs, so a
|
|
9
|
+
* journal's catch-up read after arming can never miss an append that the
|
|
10
|
+
* watch also fails to report.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { Journal, JsonlEvent } from "@effected/jsonl";
|
|
15
|
+
* import { NodeJournalWatcher } from "@effected/jsonl/node";
|
|
16
|
+
* import { NodeFileSystem } from "@effect/platform-node";
|
|
17
|
+
* import { Effect, Schema } from "effect";
|
|
18
|
+
*
|
|
19
|
+
* const events = [JsonlEvent.make("mail", { data: Schema.Struct({ round: Schema.Number }) })] as const;
|
|
20
|
+
*
|
|
21
|
+
* class Mail extends Journal.Service<Mail>()("app/Mail", { events, config: { path: ".app/mail.jsonl" } }) {}
|
|
22
|
+
*
|
|
23
|
+
* const program = Effect.gen(function* () {
|
|
24
|
+
* const mail = yield* Mail;
|
|
25
|
+
* yield* mail.append("mail", { round: 1 });
|
|
26
|
+
* }).pipe(Effect.provide(Mail.layer), Effect.provide([NodeFileSystem.layer, NodeJournalWatcher.layer]));
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export declare class NodeJournalWatcher {
|
|
32
|
+
private constructor();
|
|
33
|
+
/** Provides {@link Jsonl.JournalWatcher} over `node:fs`. */
|
|
34
|
+
static readonly layer: Layer.Layer<Jsonl.JournalWatcher>;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//# sourceMappingURL=node.d.ts.map
|
package/node.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/jsonl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Append-only, schema-validated JSONL journals as a definable Effect service.",
|
|
6
6
|
"keywords": [
|
|
@@ -34,10 +34,15 @@
|
|
|
34
34
|
"import": "./index.js",
|
|
35
35
|
"default": "./index.js"
|
|
36
36
|
},
|
|
37
|
+
"./node": {
|
|
38
|
+
"types": "./node.d.ts",
|
|
39
|
+
"import": "./node.js",
|
|
40
|
+
"default": "./node.js"
|
|
41
|
+
},
|
|
37
42
|
"./package.json": "./package.json"
|
|
38
43
|
},
|
|
39
44
|
"peerDependencies": {
|
|
40
|
-
"effect": "
|
|
45
|
+
"effect": "4.0.2"
|
|
41
46
|
},
|
|
42
47
|
"engines": {
|
|
43
48
|
"node": ">=24.11.0"
|