@smthrs/journal 0.0.0-stage → 1.0.0-rc.3
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/CHANGELOG.md +149 -0
- package/LICENSE +21 -0
- package/README.md +206 -2
- package/dist/cjs/CallFact.d.ts +129 -0
- package/dist/cjs/CallFact.d.ts.map +1 -0
- package/dist/cjs/CallFact.js +106 -0
- package/dist/cjs/CallFact.js.map +7 -0
- package/dist/cjs/Consensus.d.ts +440 -0
- package/dist/cjs/Consensus.d.ts.map +1 -0
- package/dist/cjs/Consensus.js +251 -0
- package/dist/cjs/Consensus.js.map +7 -0
- package/dist/cjs/EngineEvent.d.ts +890 -0
- package/dist/cjs/EngineEvent.d.ts.map +1 -0
- package/dist/cjs/EngineEvent.js +383 -0
- package/dist/cjs/EngineEvent.js.map +7 -0
- package/dist/cjs/ExecutionFact.d.ts +216 -0
- package/dist/cjs/ExecutionFact.d.ts.map +1 -0
- package/dist/cjs/ExecutionFact.js +303 -0
- package/dist/cjs/ExecutionFact.js.map +7 -0
- package/dist/cjs/Journal.d.ts +617 -0
- package/dist/cjs/Journal.d.ts.map +1 -0
- package/dist/cjs/Journal.js +190 -0
- package/dist/cjs/Journal.js.map +7 -0
- package/dist/cjs/JournalEvent.d.ts +421 -0
- package/dist/cjs/JournalEvent.d.ts.map +1 -0
- package/dist/cjs/JournalEvent.js +281 -0
- package/dist/cjs/JournalEvent.js.map +7 -0
- package/dist/cjs/JournalGeneration.d.ts +34 -0
- package/dist/cjs/JournalGeneration.d.ts.map +1 -0
- package/dist/cjs/JournalGeneration.js +72 -0
- package/dist/cjs/JournalGeneration.js.map +7 -0
- package/dist/cjs/JournalMetrics.d.ts +72 -0
- package/dist/cjs/JournalMetrics.d.ts.map +1 -0
- package/dist/cjs/JournalMetrics.js +55 -0
- package/dist/cjs/JournalMetrics.js.map +7 -0
- package/dist/cjs/Migrations.d.ts +42 -0
- package/dist/cjs/Migrations.d.ts.map +1 -0
- package/dist/cjs/Migrations.js +58 -0
- package/dist/cjs/Migrations.js.map +7 -0
- package/dist/cjs/OwnerId.d.ts +40 -0
- package/dist/cjs/OwnerId.d.ts.map +1 -0
- package/dist/cjs/OwnerId.js +40 -0
- package/dist/cjs/OwnerId.js.map +7 -0
- package/dist/cjs/Projection.d.ts +29 -0
- package/dist/cjs/Projection.d.ts.map +1 -0
- package/dist/cjs/Projection.js +25 -0
- package/dist/cjs/Projection.js.map +7 -0
- package/dist/cjs/RedactedLogger.d.ts +121 -0
- package/dist/cjs/RedactedLogger.d.ts.map +1 -0
- package/dist/cjs/RedactedLogger.js +235 -0
- package/dist/cjs/RedactedLogger.js.map +7 -0
- package/dist/cjs/Redaction.d.ts +350 -0
- package/dist/cjs/Redaction.d.ts.map +1 -0
- package/dist/cjs/Redaction.js +928 -0
- package/dist/cjs/Redaction.js.map +7 -0
- package/dist/cjs/SqlConsensus.d.ts +44 -0
- package/dist/cjs/SqlConsensus.d.ts.map +1 -0
- package/dist/cjs/SqlConsensus.js +243 -0
- package/dist/cjs/SqlConsensus.js.map +7 -0
- package/dist/cjs/SqlJournal.d.ts +173 -0
- package/dist/cjs/SqlJournal.d.ts.map +1 -0
- package/dist/cjs/SqlJournal.js +1568 -0
- package/dist/cjs/SqlJournal.js.map +7 -0
- package/dist/cjs/StepFact.d.ts +66 -0
- package/dist/cjs/StepFact.d.ts.map +1 -0
- package/dist/cjs/StepFact.js +68 -0
- package/dist/cjs/StepFact.js.map +7 -0
- package/dist/cjs/index.d.ts +95 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +63 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/JournalInternalFault.d.ts +37 -0
- package/dist/cjs/internal/JournalInternalFault.d.ts.map +1 -0
- package/dist/cjs/internal/JournalInternalFault.js +43 -0
- package/dist/cjs/internal/JournalInternalFault.js.map +7 -0
- package/dist/cjs/migrations/0001_initial.d.ts +15 -0
- package/dist/cjs/migrations/0001_initial.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_initial.js +54 -0
- package/dist/cjs/migrations/0001_initial.js.map +7 -0
- package/dist/cjs/migrations/0002_checkpoints.d.ts +27 -0
- package/dist/cjs/migrations/0002_checkpoints.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_checkpoints.js +48 -0
- package/dist/cjs/migrations/0002_checkpoints.js.map +7 -0
- package/dist/cjs/migrations/0003_startup_index.d.ts +15 -0
- package/dist/cjs/migrations/0003_startup_index.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_startup_index.js +40 -0
- package/dist/cjs/migrations/0003_startup_index.js.map +7 -0
- package/dist/cjs/migrations/0004_dedup.d.ts +15 -0
- package/dist/cjs/migrations/0004_dedup.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_dedup.js +57 -0
- package/dist/cjs/migrations/0004_dedup.js.map +7 -0
- package/dist/cjs/migrations/0005_run_event_type.d.ts +15 -0
- package/dist/cjs/migrations/0005_run_event_type.d.ts.map +1 -0
- package/dist/cjs/migrations/0005_run_event_type.js +40 -0
- package/dist/cjs/migrations/0005_run_event_type.js.map +7 -0
- package/dist/cjs/migrations/0006_consensus.d.ts +22 -0
- package/dist/cjs/migrations/0006_consensus.d.ts.map +1 -0
- package/dist/cjs/migrations/0006_consensus.js +84 -0
- package/dist/cjs/migrations/0006_consensus.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/Notifying.d.ts +65 -0
- package/dist/cjs/test/Notifying.d.ts.map +1 -0
- package/dist/cjs/test/Notifying.js +62 -0
- package/dist/cjs/test/Notifying.js.map +7 -0
- package/dist/cjs/test/TestJournal.d.ts +43 -0
- package/dist/cjs/test/TestJournal.d.ts.map +1 -0
- package/dist/cjs/test/TestJournal.js +43 -0
- package/dist/cjs/test/TestJournal.js.map +7 -0
- package/dist/esm/CallFact.d.ts +129 -0
- package/dist/esm/CallFact.d.ts.map +1 -0
- package/dist/esm/CallFact.js +96 -0
- package/dist/esm/CallFact.js.map +1 -0
- package/dist/esm/Consensus.d.ts +440 -0
- package/dist/esm/Consensus.d.ts.map +1 -0
- package/dist/esm/Consensus.js +454 -0
- package/dist/esm/Consensus.js.map +1 -0
- package/dist/esm/EngineEvent.d.ts +890 -0
- package/dist/esm/EngineEvent.d.ts.map +1 -0
- package/dist/esm/EngineEvent.js +614 -0
- package/dist/esm/EngineEvent.js.map +1 -0
- package/dist/esm/ExecutionFact.d.ts +216 -0
- package/dist/esm/ExecutionFact.d.ts.map +1 -0
- package/dist/esm/ExecutionFact.js +374 -0
- package/dist/esm/ExecutionFact.js.map +1 -0
- package/dist/esm/Journal.d.ts +617 -0
- package/dist/esm/Journal.d.ts.map +1 -0
- package/dist/esm/Journal.js +323 -0
- package/dist/esm/Journal.js.map +1 -0
- package/dist/esm/JournalEvent.d.ts +421 -0
- package/dist/esm/JournalEvent.d.ts.map +1 -0
- package/dist/esm/JournalEvent.js +517 -0
- package/dist/esm/JournalEvent.js.map +1 -0
- package/dist/esm/JournalGeneration.d.ts +34 -0
- package/dist/esm/JournalGeneration.d.ts.map +1 -0
- package/dist/esm/JournalGeneration.js +65 -0
- package/dist/esm/JournalGeneration.js.map +1 -0
- package/dist/esm/JournalMetrics.d.ts +72 -0
- package/dist/esm/JournalMetrics.d.ts.map +1 -0
- package/dist/esm/JournalMetrics.js +79 -0
- package/dist/esm/JournalMetrics.js.map +1 -0
- package/dist/esm/Migrations.d.ts +42 -0
- package/dist/esm/Migrations.d.ts.map +1 -0
- package/dist/esm/Migrations.js +59 -0
- package/dist/esm/Migrations.js.map +1 -0
- package/dist/esm/OwnerId.d.ts +40 -0
- package/dist/esm/OwnerId.d.ts.map +1 -0
- package/dist/esm/OwnerId.js +33 -0
- package/dist/esm/OwnerId.js.map +1 -0
- package/dist/esm/Projection.d.ts +29 -0
- package/dist/esm/Projection.d.ts.map +1 -0
- package/dist/esm/Projection.js +13 -0
- package/dist/esm/Projection.js.map +1 -0
- package/dist/esm/RedactedLogger.d.ts +121 -0
- package/dist/esm/RedactedLogger.d.ts.map +1 -0
- package/dist/esm/RedactedLogger.js +445 -0
- package/dist/esm/RedactedLogger.js.map +1 -0
- package/dist/esm/Redaction.d.ts +350 -0
- package/dist/esm/Redaction.d.ts.map +1 -0
- package/dist/esm/Redaction.js +1584 -0
- package/dist/esm/Redaction.js.map +1 -0
- package/dist/esm/SqlConsensus.d.ts +44 -0
- package/dist/esm/SqlConsensus.d.ts.map +1 -0
- package/dist/esm/SqlConsensus.js +252 -0
- package/dist/esm/SqlConsensus.js.map +1 -0
- package/dist/esm/SqlJournal.d.ts +173 -0
- package/dist/esm/SqlJournal.d.ts.map +1 -0
- package/dist/esm/SqlJournal.js +1858 -0
- package/dist/esm/SqlJournal.js.map +1 -0
- package/dist/esm/StepFact.d.ts +66 -0
- package/dist/esm/StepFact.d.ts.map +1 -0
- package/dist/esm/StepFact.js +50 -0
- package/dist/esm/StepFact.js.map +1 -0
- package/dist/esm/index.d.ts +95 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +95 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/JournalInternalFault.d.ts +37 -0
- package/dist/esm/internal/JournalInternalFault.d.ts.map +1 -0
- package/dist/esm/internal/JournalInternalFault.js +28 -0
- package/dist/esm/internal/JournalInternalFault.js.map +1 -0
- package/dist/esm/migrations/0001_initial.d.ts +15 -0
- package/dist/esm/migrations/0001_initial.d.ts.map +1 -0
- package/dist/esm/migrations/0001_initial.js +32 -0
- package/dist/esm/migrations/0001_initial.js.map +1 -0
- package/dist/esm/migrations/0002_checkpoints.d.ts +27 -0
- package/dist/esm/migrations/0002_checkpoints.d.ts.map +1 -0
- package/dist/esm/migrations/0002_checkpoints.js +38 -0
- package/dist/esm/migrations/0002_checkpoints.js.map +1 -0
- package/dist/esm/migrations/0003_startup_index.d.ts +15 -0
- package/dist/esm/migrations/0003_startup_index.d.ts.map +1 -0
- package/dist/esm/migrations/0003_startup_index.js +18 -0
- package/dist/esm/migrations/0003_startup_index.js.map +1 -0
- package/dist/esm/migrations/0004_dedup.d.ts +15 -0
- package/dist/esm/migrations/0004_dedup.d.ts.map +1 -0
- package/dist/esm/migrations/0004_dedup.js +38 -0
- package/dist/esm/migrations/0004_dedup.js.map +1 -0
- package/dist/esm/migrations/0005_run_event_type.d.ts +15 -0
- package/dist/esm/migrations/0005_run_event_type.d.ts.map +1 -0
- package/dist/esm/migrations/0005_run_event_type.js +18 -0
- package/dist/esm/migrations/0005_run_event_type.js.map +1 -0
- package/dist/esm/migrations/0006_consensus.d.ts +22 -0
- package/dist/esm/migrations/0006_consensus.d.ts.map +1 -0
- package/dist/esm/migrations/0006_consensus.js +69 -0
- package/dist/esm/migrations/0006_consensus.js.map +1 -0
- package/dist/esm/test/Notifying.d.ts +65 -0
- package/dist/esm/test/Notifying.d.ts.map +1 -0
- package/dist/esm/test/Notifying.js +62 -0
- package/dist/esm/test/Notifying.js.map +1 -0
- package/dist/esm/test/TestJournal.d.ts +43 -0
- package/dist/esm/test/TestJournal.d.ts.map +1 -0
- package/dist/esm/test/TestJournal.js +27 -0
- package/dist/esm/test/TestJournal.js.map +1 -0
- package/docs/README.md +164 -0
- package/docs/api.md +639 -0
- package/docs/concepts/compaction.md +157 -0
- package/docs/concepts/idempotency.md +127 -0
- package/docs/concepts/owner-fence.md +124 -0
- package/docs/concepts/redaction.md +218 -0
- package/docs/concepts/state-event-authority.md +159 -0
- package/docs/concepts/two-channels.md +167 -0
- package/docs/guides/commit-state-and-entry.md +114 -0
- package/docs/guides/compact-a-run.md +134 -0
- package/docs/guides/fold-a-projection.md +106 -0
- package/docs/guides/read-a-run.md +139 -0
- package/docs/guides/redact-log-output.md +94 -0
- package/docs/guides/testing.md +124 -0
- package/docs/guides/write-lifecycle-events.md +110 -0
- package/docs/installation.md +133 -0
- package/docs/quickstart.md +143 -0
- package/docs/troubleshooting.md +214 -0
- package/package.json +263 -3
- package/src/CallFact.ts +117 -0
- package/src/Consensus.ts +683 -0
- package/src/EngineEvent.ts +823 -0
- package/src/ExecutionFact.ts +433 -0
- package/src/Journal.ts +688 -0
- package/src/JournalEvent.ts +654 -0
- package/src/JournalGeneration.ts +77 -0
- package/src/JournalMetrics.ts +89 -0
- package/src/Migrations.ts +62 -0
- package/src/OwnerId.ts +42 -0
- package/src/Projection.ts +31 -0
- package/src/RedactedLogger.ts +483 -0
- package/src/Redaction.ts +1784 -0
- package/src/SqlConsensus.ts +344 -0
- package/src/SqlJournal.ts +2732 -0
- package/src/StepFact.ts +69 -0
- package/src/index.ts +109 -0
- package/src/internal/JournalInternalFault.ts +40 -0
- package/src/migrations/0001_initial.ts +41 -0
- package/src/migrations/0002_checkpoints.ts +46 -0
- package/src/migrations/0003_startup_index.ts +19 -0
- package/src/migrations/0004_dedup.ts +43 -0
- package/src/migrations/0005_run_event_type.ts +19 -0
- package/src/migrations/0006_consensus.ts +83 -0
- package/src/test/Notifying.ts +101 -0
- package/src/test/TestJournal.ts +52 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Read and follow a run"
|
|
3
|
+
description: "Choose between entries, stream, and changes; page a long history; and know which reader reports a loss and which one drops entries silently."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The journal has three reads, and they differ in exactly one dimension: what
|
|
9
|
+
happens when the reader cannot keep up. Pick by that.
|
|
10
|
+
|
|
11
|
+
| Read | Shape | Behind a fast writer |
|
|
12
|
+
| --------- | ----------------------------------------- | ----------------------------- |
|
|
13
|
+
| `entries` | one page, in sequence order | nothing is lost; you page |
|
|
14
|
+
| `stream` | replay then follow, per run | lossless; reports a sink loss |
|
|
15
|
+
| `changes` | live tail, every run this process commits | silently drops entries |
|
|
16
|
+
|
|
17
|
+
## Page a history with entries
|
|
18
|
+
|
|
19
|
+
`entries` reads one page in sequence order and reports whether another follows.
|
|
20
|
+
Use it for a poller, an export, or any bounded read:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Journal, JournalEvent } from "@smthrs/journal"
|
|
24
|
+
import * as Effect from "effect/Effect"
|
|
25
|
+
|
|
26
|
+
const readAll = (runId: JournalEvent.RunId) =>
|
|
27
|
+
Effect.gen(function*() {
|
|
28
|
+
const journal = yield* Journal.Journal
|
|
29
|
+
let after: JournalEvent.Seq | undefined = undefined
|
|
30
|
+
|
|
31
|
+
for (;;) {
|
|
32
|
+
const page: Journal.EntriesPage = after === undefined
|
|
33
|
+
? yield* journal.entries({ runId, limit: 500 })
|
|
34
|
+
: yield* journal.entries({ runId, after, limit: 500 })
|
|
35
|
+
|
|
36
|
+
for (const entry of page.entries) console.log(entry.seq, entry.eventType)
|
|
37
|
+
|
|
38
|
+
const last = page.entries.at(-1)
|
|
39
|
+
if (!page.hasMore || last === undefined) break
|
|
40
|
+
after = last.seq
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`limit` is capped at `Journal.maxEntriesLimit` (10,000). A page is decoded into
|
|
46
|
+
memory in full before the caller sees its first entry, so an unbounded limit
|
|
47
|
+
would let one call materialize a whole run.
|
|
48
|
+
|
|
49
|
+
For a nonempty list of at most 64 exact event types, use the optional `eventTypes` field on `entries`:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const finished = yield * journal.entries({ runId, eventTypes: ["build.finished", "build.failed"], limit: 100 })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This is a new public query option. Filtering happens in SQL before the page
|
|
56
|
+
limit, using the journal's `(run_id, event_type, seq)` index. `SqlJournal` is the
|
|
57
|
+
existing SQLite-specific adapter and explicitly selects that index: SQLite can
|
|
58
|
+
otherwise choose the run/sequence index for an `IN` query and scan unrelated
|
|
59
|
+
history. Multiple types may sort matching rows by sequence. The dialect-specific
|
|
60
|
+
hint stays inside `SqlJournal`; `Journal.Service` remains platform-independent. `after` remains the
|
|
61
|
+
canonical run sequence, and `hasMore` counts only matching events. Gaps between
|
|
62
|
+
matching sequences are expected. Duplicate types do not duplicate entries. No match returns an empty final page. Omitting
|
|
63
|
+
the option preserves the existing full-run read. The event type uses the same
|
|
64
|
+
bounded identifier schema as journal emission.
|
|
65
|
+
|
|
66
|
+
Filtering does not restore compacted evidence: a cursor below the run's
|
|
67
|
+
compaction floor still fails with `compacted`, even when no matching row remains.
|
|
68
|
+
Apply journal migration `0005_run_event_type` before using the updated service.
|
|
69
|
+
Custom `Journal.Service` adapters must implement the filter before a caller
|
|
70
|
+
relies on it; an older adapter may silently ignore an unknown option.
|
|
71
|
+
|
|
72
|
+
## Follow a run with stream
|
|
73
|
+
|
|
74
|
+
`stream` replays the run's durable history from `afterSequence`, then follows
|
|
75
|
+
its committed tail. It is the lossless reader, and the one to use wherever a
|
|
76
|
+
missing entry would be a correctness bug:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import * as Stream from "effect/Stream"
|
|
80
|
+
|
|
81
|
+
const follow = Effect.gen(function*() {
|
|
82
|
+
const journal = yield* Journal.Journal
|
|
83
|
+
yield* journal.stream({ runId }).pipe(
|
|
84
|
+
Stream.runForEach((entry) => Effect.log(`${entry.seq} ${entry.eventType}`))
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Three properties are worth knowing before you use it:
|
|
90
|
+
|
|
91
|
+
- It pages the history rather than materializing it, so the first entry arrives
|
|
92
|
+
after one page rather than after the whole run.
|
|
93
|
+
- It reports a sink loss that happens while it is following, to every consumer
|
|
94
|
+
that was following at the time.
|
|
95
|
+
- **It never completes.** Following is the point, so bound the stream yourself:
|
|
96
|
+
`Stream.take`, `Stream.takeUntil`, or a forked fiber you interrupt.
|
|
97
|
+
|
|
98
|
+
A cursor below the run's compaction floor fails with `compacted` and the
|
|
99
|
+
sequence to resync from, never a silently shortened history. See
|
|
100
|
+
[Checkpoints and compaction](../concepts/compaction.md).
|
|
101
|
+
|
|
102
|
+
## Tail this process with changes
|
|
103
|
+
|
|
104
|
+
`changes` is a scoped subscription to the entries this process commits, across
|
|
105
|
+
every run:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import * as PubSub from "effect/PubSub"
|
|
109
|
+
|
|
110
|
+
const tail = Effect.gen(function*() {
|
|
111
|
+
const journal = yield* Journal.Journal
|
|
112
|
+
const subscription = yield* journal.changes
|
|
113
|
+
const entry = yield* PubSub.take(subscription)
|
|
114
|
+
console.log(entry.runId, entry.seq)
|
|
115
|
+
}).pipe(Effect.scoped)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Two limits make it a local convenience rather than a reader of record:
|
|
119
|
+
|
|
120
|
+
- It is a bounded sliding buffer sized by the layer's `capacity`. A slow
|
|
121
|
+
subscriber loses entries with no error and no gap signal.
|
|
122
|
+
- Only this process's commits arrive. Another writer on the same database
|
|
123
|
+
publishes into its own buffer, which is the second reason `stream` exists: it
|
|
124
|
+
rechecks the durable tail.
|
|
125
|
+
|
|
126
|
+
Entries published to `changes` are deeply frozen, so one subscriber cannot
|
|
127
|
+
mutate the value another subscriber is about to read.
|
|
128
|
+
|
|
129
|
+
## Make a lossy write readable first
|
|
130
|
+
|
|
131
|
+
A `flush` before a read makes queued lossy entries visible. A durable entry
|
|
132
|
+
needs no flush; it was committed when its receipt arrived.
|
|
133
|
+
|
|
134
|
+
## Next steps
|
|
135
|
+
|
|
136
|
+
- [Fold a run into a projection](./fold-a-projection.md) to turn entries into a
|
|
137
|
+
served view.
|
|
138
|
+
- [Compact a long-running run](./compact-a-run.md) for what a reader below the
|
|
139
|
+
floor should do.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Keep credentials out of log output"
|
|
3
|
+
description: "Install RedactedLogger so the journal's redaction rules also cover the terminal, the JSON log document, and the spans a tracer logger exports."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 6
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The journal redacts on the write path, so a credential an action logs or
|
|
9
|
+
returns never reaches a committed row. The operator's terminal is the other
|
|
10
|
+
place the same credential surfaces: every built-in Effect logger renders through
|
|
11
|
+
the fiber's `Console`, so an action that hands a token to `Effect.logInfo`
|
|
12
|
+
writes it to stderr in full, and it reaches whatever collects that stream.
|
|
13
|
+
|
|
14
|
+
`RedactedLogger` closes that half with the same rules rather than a second rule
|
|
15
|
+
set. A log line is a diagnostic, so it takes `Redaction.diagnosticRules`: the
|
|
16
|
+
default rules plus the spellings too broad for a durable row.
|
|
17
|
+
|
|
18
|
+
## Install the layer
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { RedactedLogger } from "@smthrs/journal"
|
|
22
|
+
import * as Layer from "effect/Layer"
|
|
23
|
+
|
|
24
|
+
const logging = RedactedLogger.layer()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The layer composes over whatever loggers are already installed rather than
|
|
28
|
+
choosing one, so an operator keeps the format they had: their own logger, their
|
|
29
|
+
own layout. A host that installed its own logger keeps it too.
|
|
30
|
+
|
|
31
|
+
Pass `Redaction.Options` to extend or replace the rules, exactly as you would
|
|
32
|
+
for the journal write path. Spread `Redaction.diagnosticRules` to add to them.
|
|
33
|
+
|
|
34
|
+
## What it covers
|
|
35
|
+
|
|
36
|
+
Two halves carry load, and both are necessary:
|
|
37
|
+
|
|
38
|
+
- **The log event itself.** The message, the cause, and the log annotations are
|
|
39
|
+
redacted before the wrapped logger is handed them. This matters because a
|
|
40
|
+
logger is free to read the event instead of rendering it, and one in Effect's
|
|
41
|
+
default set does: `Logger.tracerLogger` publishes the message as a span event
|
|
42
|
+
name and the pretty cause as the span's `effect.cause` attribute, and never
|
|
43
|
+
touches `Console`. Redacting only the console would hide a credential on
|
|
44
|
+
stderr and export it in clear to whatever OTLP collector is configured.
|
|
45
|
+
Cause stack annotations are redacted too: span names, lazy stack text, parent
|
|
46
|
+
frames, and interruptor frames. The original cause is unchanged; the delegate
|
|
47
|
+
receives rebuilt reasons with redacted frame chains.
|
|
48
|
+
- **The fiber's console.** The wrapped logger is handed a view of the fiber
|
|
49
|
+
whose `Console` is a redacting console, so whatever the logger renders for
|
|
50
|
+
itself, a pretty line or a JSON document, passes through the rules on its way
|
|
51
|
+
to the stream.
|
|
52
|
+
|
|
53
|
+
Cost is bounded by the rules: each rule is linear in the length of the line,
|
|
54
|
+
with no catastrophic backtracking.
|
|
55
|
+
|
|
56
|
+
## Two differences from the write path
|
|
57
|
+
|
|
58
|
+
**A too-deep value is named, not refused.** The journal throws on a payload
|
|
59
|
+
past `Redaction.maxDepth` so the write fails with a typed `invalid_event`. A
|
|
60
|
+
logger cannot: a throw there is caught one frame up and replaces every argument
|
|
61
|
+
on the line with a placeholder, so one deep member would cost the operator the
|
|
62
|
+
whole line. `wrap` therefore defaults `onTooDeep` to `"name"`. A caller passing
|
|
63
|
+
its own `onTooDeep` still wins.
|
|
64
|
+
|
|
65
|
+
**An `Error` is rebuilt as a plain `Error`.** Its `message` and `stack` live on
|
|
66
|
+
non-enumerable properties, so the own-key rebuild the write path uses would
|
|
67
|
+
print `{}` where the operator expected a failure. The copy keeps the name,
|
|
68
|
+
message, stack, and own members with the rules applied, but not the class. A
|
|
69
|
+
copy built on the original's prototype would inherit everything the prototype
|
|
70
|
+
defines, and `name`, `cause`, `toJSON`, `Symbol.toStringTag`, and
|
|
71
|
+
`nodejs.util.inspect.custom` each carried a credential out that way.
|
|
72
|
+
|
|
73
|
+
## Wrap a single logger
|
|
74
|
+
|
|
75
|
+
Use `wrap` when you are constructing a logger rather than composing a layer:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { RedactedLogger } from "@smthrs/journal"
|
|
79
|
+
import * as Logger from "effect/Logger"
|
|
80
|
+
|
|
81
|
+
const json = RedactedLogger.wrap(Logger.formatJson)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`wrap` is idempotent. Both the CLI and the durable runtime install the layer,
|
|
85
|
+
and a detached run is both at once, so a logger this module already wrapped is
|
|
86
|
+
returned unchanged rather than paying the rules twice per line.
|
|
87
|
+
`RedactedLogger.isRedacted` reports whether a logger is already wrapped.
|
|
88
|
+
|
|
89
|
+
## Next steps
|
|
90
|
+
|
|
91
|
+
- [Redaction](../concepts/redaction.md) for the rules, the bounds, and where
|
|
92
|
+
redaction deliberately stops.
|
|
93
|
+
- [Observability](/docs/guides/observability/) for exporting the spans this
|
|
94
|
+
logger writes into.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Test against a real journal"
|
|
3
|
+
description: "Use TestJournal for the production journal over in-memory SQLite, Notifying to inject a crash at an exact transition, and the noop layer for a composition that must not write."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 7
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The journal is deterministic to test because its only nondeterministic input is
|
|
9
|
+
the database, and the database is a layer. This package ships two doubles and
|
|
10
|
+
one stub, and none of them reimplements the journal.
|
|
11
|
+
|
|
12
|
+
Neither double is exported from the root entry point, so import both by
|
|
13
|
+
subpath. `TestJournal` binds a Node SQLite database and runs on Node only;
|
|
14
|
+
`Notifying` wraps a service and is platform independent.
|
|
15
|
+
|
|
16
|
+
## Run the real journal in memory
|
|
17
|
+
|
|
18
|
+
`TestJournal.layer` provides the production `SqlJournal` over a fresh in-memory
|
|
19
|
+
database, with migrations already run:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import * as TestJournal from "@smthrs/journal/test/TestJournal"
|
|
23
|
+
|
|
24
|
+
const layer = TestJournal.layer()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`TestJournalOptions` is `SqlJournalOptions` with `capacity` and `overflow` made
|
|
28
|
+
optional, so every option forwards to `SqlJournal.layer` unchanged and a suite
|
|
29
|
+
can exercise index-bound eviction, the `maxEntryBytes` entry bound, a custom or
|
|
30
|
+
noop redactor, and the compaction policy through the bundle instead of
|
|
31
|
+
hand-assembling the layer stack the bundle exists to hide:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const strict = TestJournal.layer({
|
|
35
|
+
capacity: 8,
|
|
36
|
+
overflow: "drop-oldest",
|
|
37
|
+
sourceEventCache: 2,
|
|
38
|
+
redact: Redaction.makeNoop()
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The defaults are `capacity: 1024` and `overflow: "reject"`.
|
|
43
|
+
|
|
44
|
+
Fenced writes need the run claimed and activated through the journal's
|
|
45
|
+
`Consensus` strategy. This bundle fences through `SqlConsensus`, so a suite
|
|
46
|
+
that exercises `emitDurable`, `checkpoint`, or `compact` takes the run through
|
|
47
|
+
`Consensus.claim` and `activate` first, or takes the whole engine bundle
|
|
48
|
+
instead.
|
|
49
|
+
|
|
50
|
+
## Get every store over one database
|
|
51
|
+
|
|
52
|
+
`@smthrs/journal/test/TestJournal` provides the journal and nothing else. For
|
|
53
|
+
the journal, run, attempt, and cache services over one database, take
|
|
54
|
+
`@smthrs/engine-store/test/TestStores` from
|
|
55
|
+
[`@smthrs/engine-store`](/api/engine-store). That bundle includes the run-store
|
|
56
|
+
migrations, so the fence has a table to read.
|
|
57
|
+
|
|
58
|
+
## Inject a crash at an exact transition
|
|
59
|
+
|
|
60
|
+
`Notifying` wraps a record-of-Effect-methods service so a hook fires around
|
|
61
|
+
every operation. The hook runs in the calling fiber, so a hook that dies,
|
|
62
|
+
interrupts, or awaits a `Latch` injects crashes, fence loss, and exact
|
|
63
|
+
sequencing at any durable transition point:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { Journal } from "@smthrs/journal"
|
|
67
|
+
import * as Notifying from "@smthrs/journal/test/Notifying"
|
|
68
|
+
import * as Effect from "effect/Effect"
|
|
69
|
+
|
|
70
|
+
const crashAfterCommit = Notifying.layer(
|
|
71
|
+
Journal.Journal,
|
|
72
|
+
(op, order) => op === "emitDurable" && order === "after" ? Effect.die("crash") : Effect.void
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Three details shape how you write a hook:
|
|
77
|
+
|
|
78
|
+
- `op` is the service method name and `args` are the call arguments, so one
|
|
79
|
+
hook can select the exact call it cares about.
|
|
80
|
+
- The `after` firing only happens when the operation succeeds. A failed
|
|
81
|
+
operation is already the interesting event.
|
|
82
|
+
- Effect-returning methods and plain `Effect` properties are hooked;
|
|
83
|
+
non-Effect members, such as the `Stream`-returning `stream` and `project`,
|
|
84
|
+
delegate untouched.
|
|
85
|
+
|
|
86
|
+
`Notifying.layer` reads the real service from the environment and re-provides
|
|
87
|
+
the wrapper under the same key, so an existing stack gains a listener without
|
|
88
|
+
touching its construction. `Notifying.wrap` is the direct form, and it is
|
|
89
|
+
generic over any such service, not just the journal.
|
|
90
|
+
|
|
91
|
+
## Assert that nothing wrote
|
|
92
|
+
|
|
93
|
+
`Journal.layerNoop()` provides a closed journal whose every operation fails
|
|
94
|
+
`journal_closed`. Use it for a composition that must not write, or to prove a
|
|
95
|
+
code path never reaches the journal. Override individual methods to record
|
|
96
|
+
calls:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { Journal } from "@smthrs/journal"
|
|
100
|
+
import * as Effect from "effect/Effect"
|
|
101
|
+
|
|
102
|
+
const recorded: Array<string> = []
|
|
103
|
+
|
|
104
|
+
const layer = Journal.layerNoop({
|
|
105
|
+
emitLossy: (input) => {
|
|
106
|
+
recorded.push(input.eventType)
|
|
107
|
+
return Effect.succeed({ _tag: "Accepted", seq: 0, sourceSeq: 0 } as Journal.EmitReceipt)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The closed stub's `transact` runs its effect directly, because a stub with no
|
|
113
|
+
sink has no transaction to open and no crash window to close. A double that
|
|
114
|
+
models rollback overrides it.
|
|
115
|
+
|
|
116
|
+
## Remember the flush barrier
|
|
117
|
+
|
|
118
|
+
A test that writes on the lossy channel and then reads must `flush` in between.
|
|
119
|
+
A durable write needs no flush; it was committed when its receipt arrived.
|
|
120
|
+
|
|
121
|
+
## Next steps
|
|
122
|
+
|
|
123
|
+
- [The two channels](../concepts/two-channels.md) for what each receipt proves.
|
|
124
|
+
- [Testing flows](/docs/guides/testing-flows/) for the layer above this one.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Write a fenced lifecycle event"
|
|
3
|
+
description: "Emit a durable lifecycle entry with an OwnerId, handle fence_lost as ownership news rather than an error, and know when the unfenced channel is correct."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
A lifecycle event is the evidence that a run moved: created, started,
|
|
9
|
+
finished, failed. It belongs on the durable channel, and the durable channel is
|
|
10
|
+
fenced, so the write also proves the process still owns the run.
|
|
11
|
+
|
|
12
|
+
## Before you start
|
|
13
|
+
|
|
14
|
+
The fence is the journal's `Consensus` strategy: a fenced write commits only
|
|
15
|
+
while the strategy records the owner as holding the run, so the run must have
|
|
16
|
+
been claimed and activated first. [`@smthrs/run-store`](/api/run-store) drives
|
|
17
|
+
that for a durable run. See
|
|
18
|
+
[Installation](../installation.md#what-a-fenced-write-needs).
|
|
19
|
+
|
|
20
|
+
## Emit the entry
|
|
21
|
+
|
|
22
|
+
Pass the owner your process holds for the run:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Journal, JournalEvent } from "@smthrs/journal"
|
|
26
|
+
import type * as OwnerId from "@smthrs/journal/OwnerId"
|
|
27
|
+
import * as Effect from "effect/Effect"
|
|
28
|
+
|
|
29
|
+
const runId = "run-1" as JournalEvent.RunId
|
|
30
|
+
const sourceId = "engine" as JournalEvent.SourceId
|
|
31
|
+
|
|
32
|
+
const owner: OwnerId.OwnerId = {
|
|
33
|
+
hostId: "host-1",
|
|
34
|
+
pid: process.pid,
|
|
35
|
+
nonce: "run-1-claim"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const started = Effect.gen(function*() {
|
|
39
|
+
const journal = yield* Journal.Journal
|
|
40
|
+
return yield* journal.emitDurable({
|
|
41
|
+
runId,
|
|
42
|
+
sourceId,
|
|
43
|
+
eventType: "run.started",
|
|
44
|
+
payload: { attempt: 1 }
|
|
45
|
+
}, owner)
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The receipt arrives after the transaction commits, so the `seq` it carries is
|
|
50
|
+
already on disk. Do not advance the run or expose its result before this
|
|
51
|
+
returns: that ordering is the whole reason the channel exists.
|
|
52
|
+
|
|
53
|
+
## Handle a lost fence
|
|
54
|
+
|
|
55
|
+
`fence_lost` is not an error to retry. It is news: another process owns this
|
|
56
|
+
run now, and this one must stop writing.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const guarded = started.pipe(
|
|
60
|
+
Effect.catchTag("@smthrs/journal/JournalError", (failure) =>
|
|
61
|
+
failure.code === "fence_lost"
|
|
62
|
+
? Effect.logWarning("run reclaimed by another owner; standing down").pipe(Effect.as(undefined))
|
|
63
|
+
: Effect.fail(failure))
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Retrying with the same token cannot succeed, and reaching for
|
|
68
|
+
`emitDurableUnfenced` to get past it writes exactly the zombie entry the fence
|
|
69
|
+
exists to reject.
|
|
70
|
+
|
|
71
|
+
A neighbouring code needs a different response. `invalid_event` on a fenced
|
|
72
|
+
call means the owner you passed is not an `OwnerId`, most often a fractional or
|
|
73
|
+
negative `pid`. Fix the argument.
|
|
74
|
+
|
|
75
|
+
## Give the producer a stable identity
|
|
76
|
+
|
|
77
|
+
Supply `sourceSeq` when the caller can derive a stable number for the event.
|
|
78
|
+
The journal then dedupes a retry against the committed row instead of writing a
|
|
79
|
+
second entry:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const finished = Effect.gen(function*() {
|
|
83
|
+
const journal = yield* Journal.Journal
|
|
84
|
+
return yield* journal.emitDurable({
|
|
85
|
+
runId,
|
|
86
|
+
sourceId,
|
|
87
|
+
sourceSeq: 7 as JournalEvent.SourceSeq,
|
|
88
|
+
eventType: "run.finished",
|
|
89
|
+
payload: { outcome: "succeeded" }
|
|
90
|
+
}, owner)
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
An exact retry returns `Duplicate` with the original `seq`. A reused sequence
|
|
95
|
+
carrying different content fails `idempotency_conflict`, unless the input
|
|
96
|
+
declares `dedupe: "identity"`. See
|
|
97
|
+
[Producer identity and idempotency](../concepts/idempotency.md).
|
|
98
|
+
|
|
99
|
+
## When to use the unfenced channel
|
|
100
|
+
|
|
101
|
+
`emitDurableUnfenced` is correct for an admission that owns no run and is
|
|
102
|
+
first-writer-wins by design: an external trigger delivered by a sweeper, an
|
|
103
|
+
import, a repair tool. It is the wrong answer to a `fence_lost`.
|
|
104
|
+
|
|
105
|
+
## Next steps
|
|
106
|
+
|
|
107
|
+
- [Commit state and its entry together](./commit-state-and-entry.md) when the
|
|
108
|
+
entry describes a state transition that must not disagree with it.
|
|
109
|
+
- [The owner fence](../concepts/owner-fence.md) for what the fence checks and
|
|
110
|
+
why the token lives in this package.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Installation"
|
|
3
|
+
description: "Install @smthrs/journal, choose a database layer, install the migration sets a journal needs, and learn which extra table the fenced write path reads."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Install the package
|
|
9
|
+
|
|
10
|
+
Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
|
|
11
|
+
|
|
12
|
+
The package requires Node.js 26.4.0 or later and ships as both ESM and
|
|
13
|
+
CommonJS with TypeScript declarations. Its only runtime dependencies are
|
|
14
|
+
[`effect`](https://effect.website) and [`@smthrs/database`](/api/database).
|
|
15
|
+
Composing a journal layer imports from both of them by name, so install them as
|
|
16
|
+
direct dependencies rather than relying on them being hoisted.
|
|
17
|
+
|
|
18
|
+
`effect` is a peer dependency pinned at `4.0.0-rc.115`. Install exactly that
|
|
19
|
+
version: two copies of `effect` in one program are two sets of service tags, so
|
|
20
|
+
a journal layer built against one copy cannot be provided to a program holding
|
|
21
|
+
the other, and the mismatch shows up as a missing service rather than as a
|
|
22
|
+
version error.
|
|
23
|
+
|
|
24
|
+
## Add a database
|
|
25
|
+
|
|
26
|
+
`SqlJournal.layer` needs two services from `@smthrs/database`: a
|
|
27
|
+
`SqlClient.SqlClient` to read through and a `DurableWriter` to write through.
|
|
28
|
+
On Node, `NodeDatabase.layer` provides the client and `DurableWriter.layer()`
|
|
29
|
+
provides the writer. This composition selects the optional Node SQLite driver:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add @effect/sql-sqlite-node@4.0.0-rc.115
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import * as DurableWriter from "@smthrs/database/DurableWriter"
|
|
37
|
+
import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"
|
|
38
|
+
import * as Layer from "effect/Layer"
|
|
39
|
+
|
|
40
|
+
const database = Layer.provideMerge(
|
|
41
|
+
DurableWriter.layer(),
|
|
42
|
+
NodeDatabase.layer({ filename: "flows.db" })
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
SQLite is the supported backend at `1.0.0-rc.0`. PostgreSQL and PGlite are
|
|
47
|
+
not; see [storage compatibility](/docs/migration/compatibility#storage).
|
|
48
|
+
|
|
49
|
+
## Install the migrations
|
|
50
|
+
|
|
51
|
+
`Migrations.layer` creates this package's two tables and nothing else:
|
|
52
|
+
`flows_journal_events` with its event-type index, and
|
|
53
|
+
`flows_journal_checkpoints`.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { Migrations, SqlJournal } from "@smthrs/journal"
|
|
57
|
+
import * as Layer from "effect/Layer"
|
|
58
|
+
|
|
59
|
+
const journal = SqlJournal.layer({ capacity: 1024, overflow: "reject" }).pipe(
|
|
60
|
+
Layer.provide(Layer.provideMerge(Migrations.layer, database))
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
An application that also needs run, cache, or engine tables composes migration
|
|
65
|
+
sets rather than stacking layers. `Migrations.set` is the journal's namespaced
|
|
66
|
+
set, and `@smthrs/database`'s `Migrations.layer` runs several sets over one
|
|
67
|
+
`flows_migrations` table, giving each package a reserved id block so two
|
|
68
|
+
packages' `0001_initial` cannot collide:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import * as DatabaseMigrations from "@smthrs/database/Migrations"
|
|
72
|
+
import * as JournalMigrations from "@smthrs/journal/Migrations"
|
|
73
|
+
import * as RunStoreMigrations from "@smthrs/run-store/Migrations"
|
|
74
|
+
|
|
75
|
+
const migrations = DatabaseMigrations.layer([
|
|
76
|
+
JournalMigrations.set,
|
|
77
|
+
RunStoreMigrations.set
|
|
78
|
+
])
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`@smthrs/engine-store/Migrations` already exports `sets`, the whole durable
|
|
82
|
+
schema a durable engine needs, in dependency order. Take that list if you are
|
|
83
|
+
composing an engine rather than a standalone journal.
|
|
84
|
+
|
|
85
|
+
## What a fenced write needs
|
|
86
|
+
|
|
87
|
+
`emitDurable`, `checkpoint`, and `compact` gate their write on the `Consensus`
|
|
88
|
+
strategy the journal was built with. `SqlJournal.layer` fences through
|
|
89
|
+
`SqlConsensus`, whose lease table the journal's own migrations create, so the
|
|
90
|
+
fenced channel needs nothing beyond this package's migration set; an owner
|
|
91
|
+
takes the run through the strategy's `claim` and `activate`, which
|
|
92
|
+
[`@smthrs/run-store`](/api/run-store) drives for a durable run.
|
|
93
|
+
|
|
94
|
+
`emitLossy`, `emitDurableUnfenced`,
|
|
95
|
+
`entries`, `stream`, `project`, and `flush` read no table but the journal's
|
|
96
|
+
own, so a standalone journal serves them without run-store.
|
|
97
|
+
|
|
98
|
+
For why the fence exists and when the unfenced channel is the correct one, see
|
|
99
|
+
[the owner fence](./concepts/owner-fence.md).
|
|
100
|
+
|
|
101
|
+
## Import forms
|
|
102
|
+
|
|
103
|
+
The root entry point re-exports every module as a namespace:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { Journal, JournalEvent, Migrations, Redaction, SqlJournal } from "@smthrs/journal"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Each module is also importable from its own subpath, which is the form the API
|
|
110
|
+
reference uses:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import * as Journal from "@smthrs/journal/Journal"
|
|
114
|
+
import * as SqlJournal from "@smthrs/journal/SqlJournal"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The root is written against the driver-neutral `@smthrs/database` contract and
|
|
118
|
+
bundles for the browser. Neither test double is exported from the root, so both
|
|
119
|
+
are imported by subpath. `TestJournal` binds a Node SQLite database and runs on
|
|
120
|
+
Node only; `Notifying` wraps a service and is platform independent:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import * as Notifying from "@smthrs/journal/test/Notifying"
|
|
124
|
+
import * as TestJournal from "@smthrs/journal/test/TestJournal"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Three subpath forms are not public and are blocked in the export map:
|
|
128
|
+
`@smthrs/journal/internal/*`, `@smthrs/journal/migrations/*`, and
|
|
129
|
+
`@smthrs/journal/*/index`. `@smthrs/journal/package.json` is exported.
|
|
130
|
+
|
|
131
|
+
## Next step
|
|
132
|
+
|
|
133
|
+
Write and read one run in the [Quickstart](./quickstart.md).
|