@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
package/docs/api.md
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "API reference"
|
|
3
|
+
description: "Every public export of @smthrs/journal: the Journal service and its operations, the event envelope, the SQL layer options, redaction, projections, metrics, migrations, and the two test entry points."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import * as DurableWriter from "@smthrs/database/DurableWriter"
|
|
8
|
+
import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"
|
|
9
|
+
import { Journal, JournalEvent, Migrations, SqlJournal } from "@smthrs/journal"
|
|
10
|
+
import * as Layer from "effect/Layer"
|
|
11
|
+
|
|
12
|
+
const database = Layer.provideMerge(
|
|
13
|
+
DurableWriter.layer(),
|
|
14
|
+
NodeDatabase.layer({ filename: "flows.db" })
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const journal = SqlJournal.layer({ capacity: 1024, overflow: "reject" }).pipe(
|
|
18
|
+
Layer.provide(Layer.provideMerge(Migrations.layer, database))
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Entry points
|
|
23
|
+
|
|
24
|
+
| Import | Source | Platform |
|
|
25
|
+
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
|
26
|
+
| `@smthrs/journal` | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/src/index.ts) | any |
|
|
27
|
+
| `@smthrs/journal/test/TestJournal` | [src/test/TestJournal.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/src/test/TestJournal.ts) | Node |
|
|
28
|
+
| `@smthrs/journal/test/Notifying` | [src/test/Notifying.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/src/test/Notifying.ts) | any |
|
|
29
|
+
|
|
30
|
+
Every module in the root is also importable from `@smthrs/journal/<Module>`.
|
|
31
|
+
The root holds the journal and its contracts, written against the
|
|
32
|
+
driver-neutral `@smthrs/database` service, and it bundles for the browser. The
|
|
33
|
+
test doubles are kept out of it and live under explicit subpaths; of the two,
|
|
34
|
+
only `TestJournal` binds a Node SQLite database, which is what the platform
|
|
35
|
+
column above records. See
|
|
36
|
+
[platform support](/docs/reference/api/#platform-support).
|
|
37
|
+
|
|
38
|
+
## Journal
|
|
39
|
+
|
|
40
|
+
`Journal.Journal` is the service tag; `Journal.Service` is its interface.
|
|
41
|
+
Fallible journal operations use `JournalError` with a stable `code`.
|
|
42
|
+
|
|
43
|
+
### Operations
|
|
44
|
+
|
|
45
|
+
| Method | Signature | Behavior |
|
|
46
|
+
| --------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `emitLossy` | `(input: Input) => Effect<EmitReceipt, JournalError>` | bounded non-blocking queue; may return `Dropped` |
|
|
48
|
+
| `emitDurable` | `(input: Input, owner: OwnerId) => Effect<DurableReceipt, JournalError>` | allocates and commits inside the write transaction, fenced on `owner` |
|
|
49
|
+
| `emitDurableUnfenced` | `(input: Input) => Effect<DurableReceipt, JournalError>` | the same commit with no fence, for a genuinely ownerless admission |
|
|
50
|
+
| `transact` | `<A, E, R>(effect: Effect<A, E, R>) => Effect<A, E \| JournalError, R>` | runs a state projection and its `emitDurable` calls in one transaction |
|
|
51
|
+
| `whenCommitted` | `(update: Effect<void>) => Effect<boolean>` | accepts a short, non-failing update after managed commit, or runs it immediately outside a transaction; returns `false` without publishing when commit ownership is unknown |
|
|
52
|
+
| `stream` | `(options: StreamOptions) => Stream<Entry, JournalError>` | durable history, then committed changes; never completes |
|
|
53
|
+
| `entries` | `(options: EntriesOptions) => Effect<EntriesPage, JournalError>` | paged read |
|
|
54
|
+
| `changes` | `Effect<PubSub.Subscription<Entry>, never, Scope>` | post-commit publication, bounded and sliding |
|
|
55
|
+
| `project` | `<S, E, R>(projection: Projection<S, E, R>, options: StreamOptions) => Stream<S, JournalError, R>` | folds `stream` through a deterministic reducer |
|
|
56
|
+
| `flush` | `Effect<void, JournalError>` | barrier for the lossy queue |
|
|
57
|
+
| `checkpoint` | `(options: CheckpointOptions, owner: OwnerId) => Effect<Checkpoint, JournalError>` | durably captures replay state at a committed sequence, in `transact`'s discipline |
|
|
58
|
+
| `latestCheckpoint` | `(runId: RunId) => Effect<Option<Checkpoint>, JournalError>` | the resync point for a compacted run |
|
|
59
|
+
| `compact` | `(options: CompactOptions, owner: OwnerId) => Effect<Compacted, JournalError>` | truncates strictly below a checkpoint, atomically with the floor advance |
|
|
60
|
+
| `generation?` | `(runId: RunId) => Effect<{ generation: number; afterSeq: number }, JournalError>` | optional durable rewind generation and archive boundary; omission means generation zero |
|
|
61
|
+
|
|
62
|
+
`whenCommitted` returning `true` means accepted, possibly deferred. Failed
|
|
63
|
+
savepoints and transaction retries discard their registrations. See
|
|
64
|
+
[commit state and its entry together](./guides/commit-state-and-entry.md).
|
|
65
|
+
|
|
66
|
+
`owner` is mandatory on `emitDurable`, `checkpoint`, and `compact`. The insert,
|
|
67
|
+
the checkpoint replacement, and the truncation join the `Consensus` strategy's
|
|
68
|
+
`guard` inside their write transaction and land only while the strategy still
|
|
69
|
+
records that owner as holding the run, and otherwise fail `fence_lost`. An owner that is missing, null, or not an `OwnerId` at all fails
|
|
70
|
+
`invalid_event` instead: that is a caller contract violation, and reporting it
|
|
71
|
+
as `fence_lost` would send the caller hunting a race that never happened.
|
|
72
|
+
`emitDurableUnfenced` is the one sanctioned ownerless path, for an import or a
|
|
73
|
+
repair tool that owns no run. Reaching for it to dodge `fence_lost` writes
|
|
74
|
+
exactly the zombie entry the fence exists to reject.
|
|
75
|
+
|
|
76
|
+
`SqlJournal.layer` fences through [`SqlConsensus`](#sqlconsensus), whose lease
|
|
77
|
+
table this package's own migrations create; `SqlJournal.layerWith` fences
|
|
78
|
+
through any injected [`Consensus`](#consensus).
|
|
79
|
+
|
|
80
|
+
### Errors
|
|
81
|
+
|
|
82
|
+
`JournalError` is a `Schema.TaggedError` with the tag
|
|
83
|
+
`"@smthrs/journal/JournalError"` and the fields `code`, `message`, optional
|
|
84
|
+
`cause`, and optional `checkpointSeq`.
|
|
85
|
+
|
|
86
|
+
| Member | Type | Meaning |
|
|
87
|
+
| --------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
88
|
+
| `code` | `JournalErrorCode` | the stable classification |
|
|
89
|
+
| `message` | `string` | a human-readable description |
|
|
90
|
+
| `cause` | `unknown` (optional) | the underlying failure, for storage codes |
|
|
91
|
+
| `checkpointSeq` | `Seq` (optional) | on `compacted`, the run's floor; on `reader_behind`, the checkpoint a compaction refused to truncate below; on `checkpoint_invalid`, the floor that refused the checkpoint |
|
|
92
|
+
|
|
93
|
+
`JournalErrorCode` is one of `invalid_event`, `idempotency_conflict`,
|
|
94
|
+
`sequence_conflict`, `fence_lost`, `queue_overflow`, `journal_closed`,
|
|
95
|
+
`sink_failed`, `read_failed`, `decode_failed`, `projection_failed`,
|
|
96
|
+
`checkpoint_invalid`, `reader_behind`, `compacted`, and `unknown`. Each one is
|
|
97
|
+
diagnosed in [Troubleshooting](./troubleshooting.md).
|
|
98
|
+
|
|
99
|
+
### Receipts
|
|
100
|
+
|
|
101
|
+
| Export | Shape | Returned when |
|
|
102
|
+
| ---------------- | ------------------------------------ | -------------------------------------------------------------------------------- |
|
|
103
|
+
| `Accepted` | `{ _tag, seq, sourceSeq, evicted? }` | a new event was admitted; `evicted` counts what a `drop-oldest` policy displaced |
|
|
104
|
+
| `Duplicate` | `{ _tag, seq, sourceSeq, status }` | an exact producer retry; `status` is `"pending"` or `"committed"` |
|
|
105
|
+
| `Dropped` | `{ _tag, seq, sourceSeq, policy }` | a `drop-newest` policy discarded the event |
|
|
106
|
+
| `EmitReceipt` | `Accepted \| Duplicate \| Dropped` | the lossy channel's union |
|
|
107
|
+
| `DurableReceipt` | `Accepted \| Duplicate` | the durable channel's union; a dropped lifecycle event is unrepresentable |
|
|
108
|
+
|
|
109
|
+
### Options and models
|
|
110
|
+
|
|
111
|
+
| Export | Shape |
|
|
112
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| `OverflowPolicy` | `"reject" \| "drop-newest" \| "drop-oldest"` |
|
|
114
|
+
| `StreamOptions` | `{ runId, afterSequence? }` |
|
|
115
|
+
| `EntriesOptions` | `{ runId, after?, eventTypes?, limit }`, `limit` in `1..maxEntriesLimit`; optional nonempty list of up to 64 exact types is filtered before pagination. Cursors stay canonical; compaction checks still apply. |
|
|
116
|
+
| `EntriesPage` | `{ entries: ReadonlyArray<Entry>, hasMore: boolean }` |
|
|
117
|
+
| `CheckpointOptions` | `{ runId, seq, state }` |
|
|
118
|
+
| `Checkpoint` | `{ runId, seq, state, createdAtMs, compactedAtMs }`, the last null until a compaction truncated below it |
|
|
119
|
+
| `CompactOptions` | `{ runId, upTo? }`, defaulting to the run's latest checkpoint |
|
|
120
|
+
| `Compacted` | `{ runId, checkpointSeq, deleted }`, `deleted: 0` for a retried compaction |
|
|
121
|
+
| `maxEntriesLimit` | `10_000`, the largest page `entries` reads |
|
|
122
|
+
|
|
123
|
+
### Constructors and layers
|
|
124
|
+
|
|
125
|
+
| Export | Signature |
|
|
126
|
+
| ----------- | ------------------------------------------------------------------------------------------------ |
|
|
127
|
+
| `make` | `(implementation: Service) => Service` |
|
|
128
|
+
| `makeNoop` | `(overrides?: Partial<Service>) => Service`, every un-overridden method failing `journal_closed` |
|
|
129
|
+
| `layerNoop` | `(overrides?: Partial<Service>) => Layer<Journal>` |
|
|
130
|
+
|
|
131
|
+
The closed stub's `transact` runs its effect directly, because a stub with no
|
|
132
|
+
sink has no transaction to open. A double that models rollback overrides it.
|
|
133
|
+
|
|
134
|
+
## JournalEvent
|
|
135
|
+
|
|
136
|
+
The durable event envelope. Event types and values remain an open envelope on
|
|
137
|
+
purpose: the durable core never closes them into an interpreter-specific union.
|
|
138
|
+
|
|
139
|
+
| Export | Kind | Meaning |
|
|
140
|
+
| --------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
141
|
+
| `RunId` | branded schema and type | one durable run |
|
|
142
|
+
| `SourceId` | branded schema and type | one event producer |
|
|
143
|
+
| `Seq` | branded schema and type | canonical per-run replay order |
|
|
144
|
+
| `SourceSeq` | branded schema and type | producer-local sequence, the idempotency key |
|
|
145
|
+
| `Dedupe` | `"content" \| "identity"` | what a re-emitted producer identity means |
|
|
146
|
+
| `Input` | `Schema.Class` | `{ runId, sourceId, sourceSeq?, dedupe?, eventType, payload, meta? }` |
|
|
147
|
+
| `Entry` | `Schema.Class` | `{ runId, seq, eventId, sourceId, sourceSeq, emittedAtMs, eventType, payload, meta }` |
|
|
148
|
+
| `makeEventId` | `(runId, sourceId, sourceSeq) => string` | the deterministic durable id, length-prefixed so a separator in an identifier cannot forge another tuple |
|
|
149
|
+
| `maxIdentifierLength` | `1024` | the identifier ceiling |
|
|
150
|
+
| `companionRunId` | `(stream, runId) => RunId` | the id of the run's companion stream named `stream` |
|
|
151
|
+
| `companionPrefix` | `"flows.companion/"` | the reserved prefix of every companion stream id |
|
|
152
|
+
| `isCompanionRunId` | `(runId) => boolean` | whether a run id names a companion stream |
|
|
153
|
+
|
|
154
|
+
`RunId`, `SourceId`, and both an `Input`'s and a committed `Entry`'s
|
|
155
|
+
`eventType` are non-empty, at most `maxIdentifierLength` UTF-16 code units,
|
|
156
|
+
free of unpaired UTF-16 surrogates, and free of NUL. SQLite binds a lone
|
|
157
|
+
surrogate as U+FFFD, so two ill-formed identifiers that differ only in their
|
|
158
|
+
surrogates would land on one persisted key: the second run's first event would
|
|
159
|
+
dedupe into the first run's row, and a read by either id would return the same
|
|
160
|
+
history. SQLite's `length()` stops at the first NUL, so a NUL-bearing
|
|
161
|
+
identifier fails the column's own non-empty check and the caller is told the
|
|
162
|
+
sink failed about an identifier it had just supplied. Valid astral text is
|
|
163
|
+
ordinary text and round-trips exactly.
|
|
164
|
+
|
|
165
|
+
`Seq` and `SourceSeq` stop below `Number.MAX_SAFE_INTEGER`, so the journal can
|
|
166
|
+
always allocate the next sequence.
|
|
167
|
+
|
|
168
|
+
A companion stream carries one store's facts about a run beside the run's own
|
|
169
|
+
stream: an ordinary stream with its own sequence clock, named
|
|
170
|
+
`flows.companion/<stream>/<sha-256 of the run id>` so the id fits the
|
|
171
|
+
identifier ceiling for every run id. A consumer that reads the run's stream by
|
|
172
|
+
position never sees a companion fact, and a rewind or compaction of the run's
|
|
173
|
+
stream never truncates one. Each fact names its run in its payload.
|
|
174
|
+
`@smthrs/run-store` records ownership transitions on the `run-store` companion
|
|
175
|
+
stream, and refuses to create a run whose id begins with `companionPrefix`.
|
|
176
|
+
|
|
177
|
+
`entries`, `stream`, `checkpoint`, `latestCheckpoint`, and `compact` decode the
|
|
178
|
+
same schemas, so an identifier the writer refuses is refused on every read with
|
|
179
|
+
`invalid_event` rather than answered with an empty page.
|
|
180
|
+
|
|
181
|
+
## SqlJournal
|
|
182
|
+
|
|
183
|
+
`SqlJournal.layer(options)` provides the bounded telemetry writer and the
|
|
184
|
+
inline durable writer over `DurableWriter`.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
declare const layer: (
|
|
188
|
+
options: SqlJournalOptions
|
|
189
|
+
) => Layer.Layer<Journal, JournalError, DurableWriter | SqlClient.SqlClient>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| Option | Type | Default | Meaning |
|
|
193
|
+
| ------------------ | -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
194
|
+
| `capacity` | `number` | required | entries held in the lossy admission queue and in the sliding `changes` buffer |
|
|
195
|
+
| `overflow` | `OverflowPolicy` | required | what a full admission queue does |
|
|
196
|
+
| `batchSize` | `number` | `min(capacity, 64)` | entries the queued writer commits per transaction; `stream` reads pages of `min(batchSize, maxEntriesLimit)` |
|
|
197
|
+
| `sourceEventCache` | `number` | `4096` | upper bound on the in-process producer-idempotency index |
|
|
198
|
+
| `maxEntryBytes` | `number` | unset | largest single entry, in UTF-8 bytes of encoded `payload` plus `meta` |
|
|
199
|
+
| `redact` | `Redaction.Redactor` | `Redaction.make()` | scrub applied before encoding |
|
|
200
|
+
| `compaction` | `CompactionPolicy` | unset | automatic checkpoint-and-compact policy |
|
|
201
|
+
|
|
202
|
+
`CompactionPolicy` is `{ entryThreshold: number, capture: (runId: RunId, upTo: Seq) => Effect<unknown, unknown> }`.
|
|
203
|
+
Once a run's committed entry count reaches `entryThreshold`, the journal asks
|
|
204
|
+
`capture` for the caller's replay state at the run's durable tail, writes it as
|
|
205
|
+
a checkpoint at that sequence, and compacts below it. A capture is interrupted
|
|
206
|
+
after 30 seconds, and a failed or refused attempt is logged at warning, damped
|
|
207
|
+
for `entryThreshold` further committed entries, and never surfaced to the emit
|
|
208
|
+
that triggered it. The policy drives the internal unfenced channel, so it needs
|
|
209
|
+
no owner.
|
|
210
|
+
|
|
211
|
+
`capacity` bounds entries, never bytes. `maxEntryBytes` is checked after
|
|
212
|
+
encoding and before any sequence is allocated, so an entry over the bound fails
|
|
213
|
+
`invalid_event` and leaves no gap. It is unset by default because a running
|
|
214
|
+
engine may legitimately write large payloads and a cap introduced under one
|
|
215
|
+
would refuse writes that used to succeed.
|
|
216
|
+
|
|
217
|
+
`sourceEventCache` bounds a cache, never the authority: the database unique
|
|
218
|
+
constraint on `(run_id, source_id, source_seq)` stays authoritative, so a miss
|
|
219
|
+
changes the receipt, not the durable answer. Resident memory and startup decode
|
|
220
|
+
are proportional to this bound rather than to total history. See
|
|
221
|
+
[Producer identity and idempotency](./concepts/idempotency.md).
|
|
222
|
+
|
|
223
|
+
## OwnerId
|
|
224
|
+
|
|
225
|
+
`OwnerId.OwnerId` is the fencing token the durable channel accepts:
|
|
226
|
+
`{ hostId: string, pid: number, nonce: string }`, where `pid` is a
|
|
227
|
+
non-negative integer.
|
|
228
|
+
|
|
229
|
+
It lives here rather than with the ownership arbitration in
|
|
230
|
+
[`@smthrs/run-store`](/api/run-store) because the journal is what it fences.
|
|
231
|
+
That package's `Ownership` re-exports it alongside `LivenessEvidence`,
|
|
232
|
+
`LivenessProbe`, and the heartbeat constants.
|
|
233
|
+
|
|
234
|
+
## Consensus
|
|
235
|
+
|
|
236
|
+
The injectable strategy that arbitrates who holds a run. The journal owns the
|
|
237
|
+
rules (one writer per run, fenced and unfenced appends, commit-time
|
|
238
|
+
admission, the generation fence, steal only on staleness plus liveness
|
|
239
|
+
evidence, and ownership transitions as events), and a strategy chooses where
|
|
240
|
+
the lease lives and how the fence is checked at commit time.
|
|
241
|
+
|
|
242
|
+
| Export | Signature |
|
|
243
|
+
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
244
|
+
| `Consensus` | `Context.Service<Consensus, Service>` |
|
|
245
|
+
| `Service.claim` | `(runId, claimant: OwnerId, nowMs) => Effect<ClaimOutcome, ConsensusError>` |
|
|
246
|
+
| `Service.activate` | `(runId, owner: OwnerId, grantedAtMs, nowMs) => Effect<ActivateOutcome, ConsensusError>` |
|
|
247
|
+
| `Service.heartbeat` | `(runId, owner: OwnerId, nowMs) => Effect<HeartbeatOutcome, ConsensusError>` |
|
|
248
|
+
| `Service.reconfirm` | `(runId, owner: OwnerId, nowMs) => Effect<HeartbeatOutcome, ConsensusError>`; renews only while the owner matches and no claim is pending |
|
|
249
|
+
| `Service.release` | `(runId, owner: OwnerId) => Effect<void, ConsensusError>` |
|
|
250
|
+
| `Service.steal` | `(runId, claimant: OwnerId, nowMs, evidence: LivenessEvidence) => Effect<ClaimOutcome, ConsensusError>` |
|
|
251
|
+
| `Service.recover` | `(runId, staleClaimant: OwnerId, grantedAtMs, observer: OwnerId, nowMs, evidence: LivenessEvidence) => Effect<RecoverOutcome, ConsensusError>` |
|
|
252
|
+
| `Service.guard` | `(runId, owner: OwnerId) => Effect<void, ConsensusError>`; fails `fence_lost` unless `owner` holds the run |
|
|
253
|
+
| `ClaimOutcome` | `Claimed { grantedAtMs }` or `Rejected { reason }` |
|
|
254
|
+
| `ActivateOutcome` | `Activated` or `Lost` |
|
|
255
|
+
| `HeartbeatOutcome` | `Renewed { heartbeatAtMs }` or `Lost` |
|
|
256
|
+
| `RecoverOutcome` | `Recovered` or `Rejected { reason }` |
|
|
257
|
+
| `RejectionReason` | `already_claimed`, `owner_held`, `owner_live`, `evidence_invalid`, `claim_fresh`, `claim_changed`, `unavailable` |
|
|
258
|
+
| `ConsensusError` | `{ code: "fence_lost" \| "persistence_failed", message, cause? }` |
|
|
259
|
+
| `LivenessEvidence` | `{ expectedOwner: OwnerId, checkedAtMs, kind }` with `kind` one of `same-host-pid-dead`, `cross-host-unreachable-stale`, `lease-expired` |
|
|
260
|
+
| `matchesEvidence`, `sameOwner` | The evidence and identity predicates every strategy applies |
|
|
261
|
+
| `heartbeatInterval`, `heartbeatStaleAfter`, `heartbeatSkewAllowance`, `heartbeatWriteTolerance` | The lease durations every strategy judges staleness against |
|
|
262
|
+
| `make`, `makeNoop`, `makeLocal`, `layerNoop`, `layerLocal` | Constructors and layers; `layerLocal` is the in-memory single-process strategy |
|
|
263
|
+
|
|
264
|
+
The claim lifecycle is two-phase: `claim` (or `steal`, its evidence-gated
|
|
265
|
+
form) reserves the run and returns the grant timestamp, `activate` presents
|
|
266
|
+
that grant to take ownership, and `release` clears whichever of the caller's
|
|
267
|
+
claim or ownership still stands. `recover` clears a third party's stale claim
|
|
268
|
+
after its claimant was proven dead. Every operation takes the caller's clock
|
|
269
|
+
reading, so staleness is judged against the reading the caller observed.
|
|
270
|
+
Heartbeats and lease state never enter the journal.
|
|
271
|
+
|
|
272
|
+
`layerLocal` keeps its leases per layer build and outside any database
|
|
273
|
+
transaction: provide one instance to every service that must agree, and
|
|
274
|
+
prefer `SqlConsensus` wherever a database is present.
|
|
275
|
+
|
|
276
|
+
## SqlConsensus
|
|
277
|
+
|
|
278
|
+
The default database-backed strategy. Its lease is the `flows_consensus_leases`
|
|
279
|
+
row this package's migration `0006_consensus` creates, holding the owner tuple,
|
|
280
|
+
the two-phase claim columns, the grant timestamp, and the heartbeat. `guard`
|
|
281
|
+
is a plain SELECT inside the caller's write transaction, and every mutating
|
|
282
|
+
operation runs through `DurableWriter.write`, joining an enclosing transaction
|
|
283
|
+
as a savepoint, so a rolled-back ownership write rolls its lease back too.
|
|
284
|
+
|
|
285
|
+
| Export | Signature |
|
|
286
|
+
| ------- | -------------------------------------------------------------- |
|
|
287
|
+
| `make` | `Effect<Consensus.Service, never, DurableWriter \| SqlClient>` |
|
|
288
|
+
| `layer` | `Layer<Consensus, never, DurableWriter \| SqlClient>` |
|
|
289
|
+
|
|
290
|
+
The migration backfills a lease for every `flows_runs` row that was running
|
|
291
|
+
or claimed when the table is already present, so an owner driving a run
|
|
292
|
+
before the upgrade keeps its fence afterwards.
|
|
293
|
+
|
|
294
|
+
## Redaction
|
|
295
|
+
|
|
296
|
+
The scrub applied to every `payload` and `meta` before persistence.
|
|
297
|
+
|
|
298
|
+
| Export | Signature | Meaning |
|
|
299
|
+
| --------------------- | ------------------------------------------------ | ----------------------------------------------------------------------- |
|
|
300
|
+
| `make` | `(options?: Options) => Redactor` | builds a redactor over a rule set |
|
|
301
|
+
| `makeNoop` | `() => Redactor` | the identity redactor, for persisting verbatim by choice |
|
|
302
|
+
| `redact` | `(value: unknown, options?: Options) => unknown` | the scrub itself |
|
|
303
|
+
| `redactJsonString` | `(json: string, redactor: Redactor) => string` | scrubs an already-encoded column at a display surface |
|
|
304
|
+
| `isSensitiveKey` | `(key: string) => boolean` | whether a field name names a credential |
|
|
305
|
+
| `Redactor` | `(value: unknown) => unknown` | the function type the journal consumes |
|
|
306
|
+
| `Rule` | `{ id, pattern, replace?, rewrite? }` | one textual rule; `rewrite` for a value a regex cannot bound |
|
|
307
|
+
| `Options` | `{ rules?, onTooDeep? }` | `onTooDeep` is `"throw"` (default) or `"name"` |
|
|
308
|
+
| `defaultRules` | `ReadonlyArray<Rule>` | the built-in credential shapes |
|
|
309
|
+
| `diagnosticRules` | `ReadonlyArray<Rule>` | `defaultRules` plus the spellings too broad for a durable row |
|
|
310
|
+
| `redactDiagnostic` | `(value: unknown) => unknown` | `redact` with `diagnosticRules`, naming a too-deep value |
|
|
311
|
+
| `lineRedactor` | `(rules?: ReadonlyArray<Rule>) => LineRedactor` | redacts a stream line by line, holding a value open across lines |
|
|
312
|
+
| `LineRedactor` | `{ line, flush }` | `line` returns the lines safe to emit; `flush` ends the stream |
|
|
313
|
+
| `maxHeldLines` | `256` | lines a `LineRedactor` holds for one open value before withholding |
|
|
314
|
+
| `placeholder` | `"[REDACTED]"` | the substitution |
|
|
315
|
+
| `maxDepth` | `256` | container edges traversed before a payload is refused |
|
|
316
|
+
| `RedactionDepthError` | `{ code: "depth_exceeded", maxDepth, message }` | what `onTooDeep: "throw"` throws past `maxDepth`; a tagged error |
|
|
317
|
+
| `binaryWalkLimit` | `65_536` | bytes, and own members, before a binary view is named instead of walked |
|
|
318
|
+
| `binaryMarker` | `"[Binary]"` | a named binary view |
|
|
319
|
+
| `functionMarker` | `"[Function]"` | a named function |
|
|
320
|
+
| `symbolMarker` | `"[Symbol]"` | a named symbol |
|
|
321
|
+
| `depthMarker` | `"[Deep]"` | a value past `maxDepth`, under `onTooDeep: "name"` |
|
|
322
|
+
|
|
323
|
+
Built-in text rules cover credential assignments around `:`, `=` or `=>`, in
|
|
324
|
+
every value shape `util.inspect` and escaped JSON write, credential flags such
|
|
325
|
+
as `--password value`, Basic and Bearer authorization, bare JWTs, private-key
|
|
326
|
+
PEM and PGP blocks (including OpenSSH, and a block with no footer), and
|
|
327
|
+
cookie/session assignments. Structured
|
|
328
|
+
credential suffixes also include `secretAccessKey`, `passphrase`, `sshKey`,
|
|
329
|
+
`session`, `signature`, and `auth`. See the full
|
|
330
|
+
[redaction rules](./concepts/redaction.md).
|
|
331
|
+
|
|
332
|
+
Rows are permanent and are replayed verbatim to sync subscribers and
|
|
333
|
+
time-travel consumers, so redaction on write is the only place it can be
|
|
334
|
+
enforced once. The rule set is a best-effort textual net over shapes seen in
|
|
335
|
+
real reports, not a proof: a value that must never persist belongs in a
|
|
336
|
+
`Redacted` field of the caller's own schema.
|
|
337
|
+
|
|
338
|
+
Redaction stops at the journal. It is an observability concern, and journal
|
|
339
|
+
rows exist to be read. The stores in [`@smthrs/run-store`](/api/run-store) and
|
|
340
|
+
[`@smthrs/step-cache`](/api/step-cache) hold executable state and are
|
|
341
|
+
deliberately not redacted; neither takes a `redact` option at all. A journal
|
|
342
|
+
`Checkpoint`'s `state` is executable state for the same reason and is not
|
|
343
|
+
redacted either.
|
|
344
|
+
|
|
345
|
+
`redactJsonString` returns a string that does not parse untouched, because
|
|
346
|
+
validation is the caller's. Once parsing succeeds it fails closed: a throwing
|
|
347
|
+
redactor or an encoding failure returns the JSON string `"[REDACTED]"`, never
|
|
348
|
+
the original text.
|
|
349
|
+
|
|
350
|
+
Full behavior, including the bounds on hostile input, is in
|
|
351
|
+
[Redaction](./concepts/redaction.md).
|
|
352
|
+
|
|
353
|
+
## RedactedLogger
|
|
354
|
+
|
|
355
|
+
The same rules applied to log output.
|
|
356
|
+
|
|
357
|
+
| Export | Signature |
|
|
358
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
|
|
359
|
+
| `layer` | `(options?: Redaction.Options) => Layer<never>` |
|
|
360
|
+
| `wrap` | `<Message, Output>(logger: Logger<Message, Output>, options?: Redaction.Options) => Logger<Message, Output>` |
|
|
361
|
+
| `isRedacted` | `(logger: Logger<any, any>) => boolean` |
|
|
362
|
+
| `redactArgument` | `(value: unknown, redactor: Redaction.Redactor) => unknown` |
|
|
363
|
+
| `redactingConsole` | `(target: Console, redactor: Redaction.Redactor) => Console` |
|
|
364
|
+
| `TypeId` | `unique symbol`, the marker `wrap` sets |
|
|
365
|
+
|
|
366
|
+
`layer` replaces the active logger set with redacting wrappers of the same
|
|
367
|
+
loggers, so an operator keeps the format they had. `wrap` redacts the log event
|
|
368
|
+
itself, the message, the cause, and the annotations, and hands the wrapped
|
|
369
|
+
logger a fiber whose `Console` is redacting. Both halves are load-bearing:
|
|
370
|
+
`Logger.tracerLogger` reads the event and never touches `Console`.
|
|
371
|
+
|
|
372
|
+
`wrap` defaults `onTooDeep` to `"name"`, because a throw in a logger costs the
|
|
373
|
+
operator the whole line. A caller's own `onTooDeep` still wins. Wrapping is
|
|
374
|
+
idempotent.
|
|
375
|
+
|
|
376
|
+
See [Keep credentials out of log output](./guides/redact-log-output.md).
|
|
377
|
+
|
|
378
|
+
## Projection
|
|
379
|
+
|
|
380
|
+
| Export | Signature |
|
|
381
|
+
| --------------------- | ----------------------------------------------------------------------------------- |
|
|
382
|
+
| `Projection<S, E, R>` | `{ name: string, initial: S, reduce: (state: S, entry: Entry) => Effect<S, E, R> }` |
|
|
383
|
+
| `make` | `<S, E, R>(projection: Projection<S, E, R>) => Projection<S, E, R>` |
|
|
384
|
+
|
|
385
|
+
Projections have no independent durable state. Replaying the same entries
|
|
386
|
+
through the same reducer must reproduce the same result. `Journal.project`
|
|
387
|
+
emits `initial` first, then one state per entry, and follows the run's tail
|
|
388
|
+
without completing. See
|
|
389
|
+
[Fold a run into a projection](./guides/fold-a-projection.md).
|
|
390
|
+
|
|
391
|
+
## JournalMetrics
|
|
392
|
+
|
|
393
|
+
| Export | Type |
|
|
394
|
+
| ------------- | --------------------------------------------------------------------------- |
|
|
395
|
+
| `writes` | the `flows_journal_writes` counter, dimensioned by `channel` and `receipt` |
|
|
396
|
+
| `durable` | `writes` views keyed by `"Accepted" \| "Duplicate"` |
|
|
397
|
+
| `lossy` | `writes` views keyed by `"Accepted" \| "Duplicate" \| "Dropped"` |
|
|
398
|
+
| `lostEntries` | the `flows_journal_lost` counter, dimensioned by `channel` and error `code` |
|
|
399
|
+
| `lost(code)` | the `lostEntries` view for one `JournalError` code |
|
|
400
|
+
|
|
401
|
+
`SqlJournal` updates these once per emission receipt, so they measure
|
|
402
|
+
admissions on the hot path rather than rows read back. A durable emission
|
|
403
|
+
counts when its receipt is produced; under `transact` that is still inside the
|
|
404
|
+
caller's transaction, so a receipt that later rolls back has already counted.
|
|
405
|
+
The counter is throughput evidence, not commit evidence.
|
|
406
|
+
|
|
407
|
+
`lostEntries` moves by the number of admitted lossy entries the writer failed
|
|
408
|
+
to commit, when it gives them up, whether or not a `flush` is waiting. The
|
|
409
|
+
writer also logs a warning naming the error code, the count, and the runs. A
|
|
410
|
+
final flush that fails while the journal closes logs a warning too.
|
|
411
|
+
|
|
412
|
+
No exporter ships in this package. Provide one, for example
|
|
413
|
+
[`@smthrs/observability`](/api/observability), and these counters appear in it.
|
|
414
|
+
|
|
415
|
+
## Migrations
|
|
416
|
+
|
|
417
|
+
| Export | Signature |
|
|
418
|
+
| ------- | -------------------------------------------------------------------------- |
|
|
419
|
+
| `set` | `DatabaseMigrations.MigrationSet`, namespace `"journal"`, id block `0` |
|
|
420
|
+
| `run` | `Effect<ReadonlyArray<[id, name]>, MigrationError \| SqlError, SqlClient>` |
|
|
421
|
+
| `layer` | `Layer<never, MigrationError \| SqlError, SqlClient>` |
|
|
422
|
+
|
|
423
|
+
`0001_initial` creates `flows_journal_events` and its event-type index;
|
|
424
|
+
`0002_checkpoints` creates `flows_journal_checkpoints`;
|
|
425
|
+
`0003_startup_index` indexes the timestamp/run/sequence ordering used by the
|
|
426
|
+
bounded startup query. `run` and `layer` install this package's full set.
|
|
427
|
+
The migration composer also applies the new index to an installed journal
|
|
428
|
+
block when another package has already advanced the global migration cursor.
|
|
429
|
+
|
|
430
|
+
Every other durable table belongs to the package that reads it.
|
|
431
|
+
`@smthrs/database`'s `Migrations` composes several sets over one
|
|
432
|
+
`flows_migrations` table, namespacing each package's ids into a reserved block
|
|
433
|
+
so two packages' `0001_initial` cannot collide.
|
|
434
|
+
`@smthrs/engine-store/Migrations` exports `sets`, the composed list a durable
|
|
435
|
+
engine installs. No version shipped before `1.0.0-rc.0`, so each package
|
|
436
|
+
carries one authoritative initial schema rather than compatibility migrations
|
|
437
|
+
for schemas no published release ever used.
|
|
438
|
+
|
|
439
|
+
## Test entry points
|
|
440
|
+
|
|
441
|
+
`@smthrs/journal/test/TestJournal` provides the production SQL journal over a
|
|
442
|
+
migrated in-memory database:
|
|
443
|
+
|
|
444
|
+
| Export | Signature |
|
|
445
|
+
| -------------------- | ----------------------------------------------------------- |
|
|
446
|
+
| `layer` | `(options?: TestJournalOptions) => Layer<Journal, ...>` |
|
|
447
|
+
| `TestJournalOptions` | `SqlJournalOptions` with `capacity` and `overflow` optional |
|
|
448
|
+
|
|
449
|
+
`TestJournalOptions` is the production option type, so every field forwards to
|
|
450
|
+
`SqlJournal.layer` unchanged and a new production option is reachable here the
|
|
451
|
+
day it is added. The defaults are `capacity: 1024` and `overflow: "reject"`.
|
|
452
|
+
This bundle creates the journal's tables, the consensus lease table included,
|
|
453
|
+
so a suite exercising a fenced call takes the run through `Consensus.claim`
|
|
454
|
+
and `activate`, or takes `@smthrs/engine-store/test/TestStores`, which
|
|
455
|
+
provides the journal, run, attempt, and cache services over one database.
|
|
456
|
+
|
|
457
|
+
`@smthrs/journal/test/Notifying` wraps a record-of-Effect-methods service so a
|
|
458
|
+
hook fires around every operation:
|
|
459
|
+
|
|
460
|
+
| Export | Signature |
|
|
461
|
+
| ------- | ------------------------------------------------------------------------------------- |
|
|
462
|
+
| `Order` | `"before" \| "after"` |
|
|
463
|
+
| `Hook` | `(op: string, order: Order, args: ReadonlyArray<unknown>) => Effect<void>` |
|
|
464
|
+
| `wrap` | `<S extends object>(service: S, hook: Hook) => S` |
|
|
465
|
+
| `layer` | `<Id, S extends object>(tag: Context.Key<Id, S>, hook: Hook) => Layer<Id, never, Id>` |
|
|
466
|
+
|
|
467
|
+
The hook runs in the calling fiber, so a hook that dies, interrupts, or awaits
|
|
468
|
+
a `Latch` injects crashes, fence loss, and exact sequencing at any durable
|
|
469
|
+
transition point. The `after` firing only happens when the operation succeeds.
|
|
470
|
+
Effect-returning methods and plain `Effect` properties are hooked; non-Effect
|
|
471
|
+
members, such as `stream` and `project`, delegate untouched.
|
|
472
|
+
|
|
473
|
+
See [Test against a real journal](./guides/testing.md).
|
|
474
|
+
|
|
475
|
+
## Sequence allocation
|
|
476
|
+
|
|
477
|
+
`emitDurable` allocates both sequences inside the writer's transaction
|
|
478
|
+
(`MAX(seq) + 1`, taking the in-memory clock as a floor) and inserts the row
|
|
479
|
+
before returning, so the returned `seq` is already committed. Use it wherever a
|
|
480
|
+
caller acts on the returned sequence: lifecycle finalization, cross-process
|
|
481
|
+
supervisors, or any deployment where a second writer may open the same run. A
|
|
482
|
+
durable boundary must not advance the run or expose its result until this
|
|
483
|
+
commit returns.
|
|
484
|
+
|
|
485
|
+
`Seq` is canonical per-run replay order and `SourceSeq` identifies producer
|
|
486
|
+
retries. Rejected and dropped admissions may consume either sequence, so gaps
|
|
487
|
+
are valid: allocation is `MAX(seq) + 1` and replay is `ORDER BY seq`, so
|
|
488
|
+
neither reads a gap as anything.
|
|
489
|
+
|
|
490
|
+
## Idempotency equality
|
|
491
|
+
|
|
492
|
+
`(runId, sourceId, sourceSeq)` is the producer identity, unique in the database
|
|
493
|
+
and addressed by the deterministic `makeEventId`. Two emissions under one
|
|
494
|
+
identity are the same event when their event type and their canonically
|
|
495
|
+
encoded, redacted `payload` and `meta` match. The encoding sorts object keys,
|
|
496
|
+
so key order does not change the answer. An exact producer retry returns
|
|
497
|
+
`Duplicate` with `status: "committed"`; a reused producer sequence carrying
|
|
498
|
+
different content fails `idempotency_conflict` unless the input declares
|
|
499
|
+
`dedupe: "identity"`.
|
|
500
|
+
|
|
501
|
+
The persisted bytes are `JSON.stringify` semantics with object keys sorted: a
|
|
502
|
+
`Date` is its ISO string, an `undefined` member is dropped, `NaN` is `null`.
|
|
503
|
+
Sorting happens over the encoded JSON, never over the raw value, so
|
|
504
|
+
canonicalization cannot destroy anything the encoder would have kept.
|
|
505
|
+
|
|
506
|
+
Two consequences follow from comparing the persisted, redacted bytes. Two
|
|
507
|
+
different secrets that redact to the same placeholder are the same event to the
|
|
508
|
+
journal, and `NaN` and `null` are the same value. Comparing the pre-redaction
|
|
509
|
+
value instead would keep unredacted secrets resident in the in-process index,
|
|
510
|
+
which is the leak this package exists to prevent, and the durable re-check at
|
|
511
|
+
insert can only read the persisted bytes.
|
|
512
|
+
|
|
513
|
+
## transact
|
|
514
|
+
|
|
515
|
+
Committing an entry makes that entry durable; it does not by itself make a
|
|
516
|
+
host's whole view crash-consistent, because the executable state lives in the
|
|
517
|
+
neighboring stores: `RunStore` and `AttemptStore` in
|
|
518
|
+
[`@smthrs/run-store`](/api/run-store), `CacheStore` in
|
|
519
|
+
[`@smthrs/step-cache`](/api/step-cache), and `DurableEngineState` in
|
|
520
|
+
[`@smthrs/engine-store`](/api/engine-store). `transact` closes that seam: those stores write through the same `DurableWriter`, so their
|
|
521
|
+
writes join this transaction as savepoints and the row and its lifecycle entry
|
|
522
|
+
either both commit or both roll back.
|
|
523
|
+
|
|
524
|
+
Three properties matter to callers:
|
|
525
|
+
|
|
526
|
+
- **Publication follows COMMIT.** Inside a transaction, `emitDurable` returning
|
|
527
|
+
means a savepoint was released. The `changes` and `stream` publish and the
|
|
528
|
+
in-process producer index update are parked until the outermost transaction
|
|
529
|
+
commits, so a subscriber never sees an entry that later rolls back, and a
|
|
530
|
+
rolled-back producer identity stays re-emittable instead of deduplicating
|
|
531
|
+
against a sequence that does not exist.
|
|
532
|
+
- **Only storage work belongs inside.** The transaction is held for its whole
|
|
533
|
+
body: no flow bodies, host calls, or `flush`, which waits on the lossy writer
|
|
534
|
+
and would deadlock against the open transaction.
|
|
535
|
+
- **Nesting is a savepoint.** An inner `transact` defers its settlements to the
|
|
536
|
+
outermost commit.
|
|
537
|
+
|
|
538
|
+
A crash before COMMIT still loses the whole unit, so work that had already run,
|
|
539
|
+
an action body for instance, re-executes on the next drive. And no local
|
|
540
|
+
transaction makes a remote effect atomic, so external effects still need
|
|
541
|
+
idempotency keys, fencing tokens, or compensation. The transaction can also be
|
|
542
|
+
replayed by the database package's write retry, which is a second reason a body
|
|
543
|
+
must tolerate running more than once; see
|
|
544
|
+
[Commit state and its entry together](./guides/commit-state-and-entry.md#a-note-on-retries).
|
|
545
|
+
|
|
546
|
+
## Failure and loss
|
|
547
|
+
|
|
548
|
+
The two channels fail independently, and neither failure is permanent. A batch
|
|
549
|
+
the optimistic writer cannot persist is lost and reported: to the `flush`
|
|
550
|
+
waiters that covered it, to live `stream` consumers that were following when it
|
|
551
|
+
happened, and, if nobody was waiting, to the next `flush`. The writer fiber
|
|
552
|
+
survives it. Each loss is reported once; a later `flush` with nothing
|
|
553
|
+
outstanding succeeds, while entries queued behind the lost batch stay
|
|
554
|
+
outstanding, so a subsequent `flush` still waits for them instead of vouching
|
|
555
|
+
for unpersisted work. `emitDurable` was never gated by the queue: it opens its
|
|
556
|
+
own transaction inline, so the lossless lifecycle channel keeps working as soon
|
|
557
|
+
as the database is healthy again.
|
|
558
|
+
|
|
559
|
+
`changes` is a bounded sliding buffer sized by the layer's `capacity`: a slow
|
|
560
|
+
subscriber loses entries with no error and no gap signal. `stream` is the
|
|
561
|
+
lossless follower. Entries published to `changes` are frozen, so one subscriber
|
|
562
|
+
cannot mutate another's view.
|
|
563
|
+
|
|
564
|
+
Reads below a run's compaction floor fail with `compacted`. See
|
|
565
|
+
[Checkpoints and compaction](./concepts/compaction.md) and the
|
|
566
|
+
[`@smthrs/journal` error codes](/docs/reference/errors/#smthrsjournal).
|
|
567
|
+
|
|
568
|
+
## Resource limits
|
|
569
|
+
|
|
570
|
+
| Limit | Value |
|
|
571
|
+
| ------------------------------------ | ---------------------------------------------------------------------- |
|
|
572
|
+
| admission queue and `changes` buffer | `capacity` entries |
|
|
573
|
+
| one entry | `maxEntryBytes` UTF-8 bytes of `payload` plus `meta`, unset by default |
|
|
574
|
+
| run ids, source ids, event types | 1,024 UTF-16 code units |
|
|
575
|
+
| `Seq`, `SourceSeq` | below `Number.MAX_SAFE_INTEGER` |
|
|
576
|
+
| one `entries` page | `maxEntriesLimit`, 10,000 entries |
|
|
577
|
+
| producer-idempotency index | `sourceEventCache` entries, default 4,096 |
|
|
578
|
+
| redaction traversal | `maxDepth`, 256 container edges |
|
|
579
|
+
| binary view walk | `binaryWalkLimit`, 65,536 bytes or own members |
|
|
580
|
+
|
|
581
|
+
A payload deeper than `Redaction.maxDepth` fails `invalid_event` rather than
|
|
582
|
+
overflowing the stack, and the canonical encoder carries the same ceiling for a
|
|
583
|
+
caller who disables redaction.
|
|
584
|
+
|
|
585
|
+
## See also
|
|
586
|
+
|
|
587
|
+
- [State and event authority](./concepts/state-event-authority.md): shared
|
|
588
|
+
bounded identities, `EngineEvent` schemas, constructors and consumer decoders.
|
|
589
|
+
- [Durable execution](/docs/concepts/durable-execution/) and
|
|
590
|
+
[Execution IDs and ownership](/docs/concepts/ownership/).
|
|
591
|
+
- [`@smthrs/engine-store`](/api/engine-store), which composes this package with
|
|
592
|
+
the run store and the step cache.
|
|
593
|
+
- [`@smthrs/chain`](/api/chain) has a journal of its own with a different
|
|
594
|
+
contract; see [the chain journal](/pkg/chain/concepts/journal).
|
|
595
|
+
|
|
596
|
+
### Journal generations
|
|
597
|
+
|
|
598
|
+
`Journal.Service.generation?(runId)` returns an Effect of
|
|
599
|
+
`{ generation: number, afterSeq: number }` with `JournalError` failures.
|
|
600
|
+
Append-only adapters may omit it (generation zero). A truncating adapter must
|
|
601
|
+
advance it atomically with truncation so followers detect reused sequences.
|
|
602
|
+
`SqlJournal.layer` persists it in `flows_journal_generations`, installed through
|
|
603
|
+
`JournalGeneration.initialize`; a fresh run reports `{ generation: 0, afterSeq: -1 }`.
|
|
604
|
+
|
|
605
|
+
`JournalGeneration.forget(runIds)` invalidates live journal source identities and
|
|
606
|
+
allocation floors after a committed truncation, for every journal sharing the
|
|
607
|
+
SQL client. It requires `SqlClient`; it changes no durable rows. Producers must
|
|
608
|
+
be quiescent and pending lossy admissions flushed before truncation.
|
|
609
|
+
`JournalGeneration.onTruncate(callback)` registers an invalidator for the current
|
|
610
|
+
scope. `SqlJournal` registers automatically, and `SqlTimeTravelStore.archiveAndTruncate`
|
|
611
|
+
notifies after commit for the parent and attached descendants. Re-emitting an
|
|
612
|
+
archived lossy source identity is then admitted against the retained history.
|
|
613
|
+
|
|
614
|
+
## Native execution facts and human waits
|
|
615
|
+
|
|
616
|
+
`ExecutionFact.fold` verifies native run-local sequence/generation evidence and
|
|
617
|
+
reconstructs root identity, current trampoline lifecycle, and attached descendant
|
|
618
|
+
human waits. Current observations include the additive `treeVersion: 1`,
|
|
619
|
+
`parentPolicy`, and redacted `waiting.request`/`waiting.point` metadata. Detached
|
|
620
|
+
subtrees do not roll their questions into an ancestor's status. A settled wait
|
|
621
|
+
leaves the derived open-wait set.
|
|
622
|
+
|
|
623
|
+
`Provenance.humanWaits` distinguishes covered events from legacy or unverified
|
|
624
|
+
observations. Old version-1 facts lacking these fields cannot establish tree-wait
|
|
625
|
+
coverage. `humanWaitSources` records separate generation and baseline sequence
|
|
626
|
+
bounds for each native execution in the derived tree, so upgrading an old stream
|
|
627
|
+
does not claim question history before the extension first appeared. A missing
|
|
628
|
+
child stream, explicit gap, incompatible metadata, or question that differs from
|
|
629
|
+
the coherent observed tree refuses that coverage. The wait set compares by
|
|
630
|
+
execution identity and exact semantic values; adapter enumeration order does not
|
|
631
|
+
change coverage. The derived list orders by depth, creation time, and lexical ID.
|
|
632
|
+
The root/current semantic provenance also declines a complete event claim when
|
|
633
|
+
the requested tree comparison fails.
|
|
634
|
+
|
|
635
|
+
The fact is display evidence. Its token digest does not authorize a signal or
|
|
636
|
+
reconstruct an opaque resolver token. The gateway joins a verified question to
|
|
637
|
+
a currently observed wait with the same execution identity and digest, then
|
|
638
|
+
uses that current wait's existing submission envelope. A historical question
|
|
639
|
+
alone cannot make a gate answerable after the live wait has ended.
|