@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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `SqlConsensus` lease table.
|
|
3
|
+
*
|
|
4
|
+
* This is the private schema of the default consensus strategy: the owner
|
|
5
|
+
* tuple, the two-phase claim columns, and the grant timestamp relocated from
|
|
6
|
+
* `flows_runs`. Lease rows are strategy evidence, never history: heartbeats
|
|
7
|
+
* and lease state never enter the journal.
|
|
8
|
+
*
|
|
9
|
+
* @since 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
import * as Dialect from "@smthrs/database/Dialect";
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
14
|
+
/**
|
|
15
|
+
* Creates the `flows_consensus_leases` table and backfills a lease for every
|
|
16
|
+
* running or claimed run when `flows_runs` is already present, so an owner
|
|
17
|
+
* that was driving a run before this migration keeps its fence afterwards.
|
|
18
|
+
*
|
|
19
|
+
* @category migrations
|
|
20
|
+
* @since 1.0.0
|
|
21
|
+
*/
|
|
22
|
+
export const consensus = Effect.gen(function* () {
|
|
23
|
+
const sql = yield* SqlClient.SqlClient;
|
|
24
|
+
yield* sql `CREATE TABLE flows_consensus_leases (
|
|
25
|
+
run_id TEXT PRIMARY KEY CHECK (length(run_id) > 0),
|
|
26
|
+
owner_host_id TEXT,
|
|
27
|
+
owner_pid ${Dialect.integer(sql)} CHECK (owner_pid IS NULL OR (${Dialect.isInteger(sql, sql `owner_pid`)} AND owner_pid >= 0)),
|
|
28
|
+
owner_nonce TEXT,
|
|
29
|
+
granted_at_ms ${Dialect.integer(sql)} CHECK (granted_at_ms IS NULL OR (${Dialect.isInteger(sql, sql `granted_at_ms`)} AND granted_at_ms >= 0)),
|
|
30
|
+
heartbeat_at_ms ${Dialect.integer(sql)} CHECK (heartbeat_at_ms IS NULL OR (${Dialect.isInteger(sql, sql `heartbeat_at_ms`)} AND heartbeat_at_ms >= 0)),
|
|
31
|
+
claim_host_id TEXT,
|
|
32
|
+
claim_pid ${Dialect.integer(sql)} CHECK (claim_pid IS NULL OR (${Dialect.isInteger(sql, sql `claim_pid`)} AND claim_pid >= 0)),
|
|
33
|
+
claim_nonce TEXT,
|
|
34
|
+
claimed_at_ms ${Dialect.integer(sql)} CHECK (claimed_at_ms IS NULL OR (${Dialect.isInteger(sql, sql `claimed_at_ms`)} AND claimed_at_ms >= 0))
|
|
35
|
+
)`;
|
|
36
|
+
const tables = yield* Dialect.tables(sql);
|
|
37
|
+
if (!tables.some((table) => table.name === "flows_runs")) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
yield* sql `
|
|
41
|
+
INSERT INTO flows_consensus_leases (
|
|
42
|
+
run_id,
|
|
43
|
+
owner_host_id,
|
|
44
|
+
owner_pid,
|
|
45
|
+
owner_nonce,
|
|
46
|
+
granted_at_ms,
|
|
47
|
+
heartbeat_at_ms,
|
|
48
|
+
claim_host_id,
|
|
49
|
+
claim_pid,
|
|
50
|
+
claim_nonce,
|
|
51
|
+
claimed_at_ms
|
|
52
|
+
)
|
|
53
|
+
SELECT
|
|
54
|
+
run_id,
|
|
55
|
+
owner_host_id,
|
|
56
|
+
owner_pid,
|
|
57
|
+
owner_nonce,
|
|
58
|
+
CASE WHEN status = 'running' THEN heartbeat_at_ms ELSE NULL END,
|
|
59
|
+
CASE WHEN status = 'running' THEN heartbeat_at_ms ELSE NULL END,
|
|
60
|
+
claim_host_id,
|
|
61
|
+
claim_pid,
|
|
62
|
+
claim_nonce,
|
|
63
|
+
claimed_at_ms
|
|
64
|
+
FROM flows_runs
|
|
65
|
+
WHERE status = 'running' OR claim_host_id IS NOT NULL
|
|
66
|
+
ON CONFLICT (run_id) DO NOTHING
|
|
67
|
+
`;
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=0006_consensus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0006_consensus.js","sourceRoot":"","sources":["../../../src/migrations/0006_consensus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,OAAO,MAAM,0BAA0B,CAAA;AACnD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,+BAA+B,CAAA;AAE1D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAsD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC9F,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,SAAS,CAAA;IAEtC,KAAK,CAAC,CAAC,GAAG,CAAA;;;gBAGI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,iCAChC,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,WAAW,CACvC;;oBAEkB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,qCACpC,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,eAAe,CAC3C;sBACoB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,uCACtC,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,iBAAiB,CAC7C;;gBAEc,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,iCAChC,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,WAAW,CACvC;;oBAEkB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,qCACpC,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,eAAe,CAC3C;IACE,CAAA;IAEF,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACzC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC,EAAE,CAAC;QACzD,OAAM;IACR,CAAC;IAED,KAAK,CAAC,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BT,CAAA;AACH,CAAC,CAAC,CAAA","sourcesContent":["/**\n * The `SqlConsensus` lease table.\n *\n * This is the private schema of the default consensus strategy: the owner\n * tuple, the two-phase claim columns, and the grant timestamp relocated from\n * `flows_runs`. Lease rows are strategy evidence, never history: heartbeats\n * and lease state never enter the journal.\n *\n * @since 1.0.0\n */\n\nimport * as Dialect from \"@smthrs/database/Dialect\"\nimport * as Effect from \"effect/Effect\"\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\"\n\n/**\n * Creates the `flows_consensus_leases` table and backfills a lease for every\n * running or claimed run when `flows_runs` is already present, so an owner\n * that was driving a run before this migration keeps its fence afterwards.\n *\n * @category migrations\n * @since 1.0.0\n */\nexport const consensus: Effect.Effect<void, unknown, SqlClient.SqlClient> = Effect.gen(function*() {\n const sql = yield* SqlClient.SqlClient\n\n yield* sql`CREATE TABLE flows_consensus_leases (\n run_id TEXT PRIMARY KEY CHECK (length(run_id) > 0),\n owner_host_id TEXT,\n owner_pid ${Dialect.integer(sql)} CHECK (owner_pid IS NULL OR (${\n Dialect.isInteger(sql, sql`owner_pid`)\n } AND owner_pid >= 0)),\n owner_nonce TEXT,\n granted_at_ms ${Dialect.integer(sql)} CHECK (granted_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`granted_at_ms`)\n } AND granted_at_ms >= 0)),\n heartbeat_at_ms ${Dialect.integer(sql)} CHECK (heartbeat_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`heartbeat_at_ms`)\n } AND heartbeat_at_ms >= 0)),\n claim_host_id TEXT,\n claim_pid ${Dialect.integer(sql)} CHECK (claim_pid IS NULL OR (${\n Dialect.isInteger(sql, sql`claim_pid`)\n } AND claim_pid >= 0)),\n claim_nonce TEXT,\n claimed_at_ms ${Dialect.integer(sql)} CHECK (claimed_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`claimed_at_ms`)\n } AND claimed_at_ms >= 0))\n )`\n\n const tables = yield* Dialect.tables(sql)\n if (!tables.some((table) => table.name === \"flows_runs\")) {\n return\n }\n\n yield* sql`\n INSERT INTO flows_consensus_leases (\n run_id,\n owner_host_id,\n owner_pid,\n owner_nonce,\n granted_at_ms,\n heartbeat_at_ms,\n claim_host_id,\n claim_pid,\n claim_nonce,\n claimed_at_ms\n )\n SELECT\n run_id,\n owner_host_id,\n owner_pid,\n owner_nonce,\n CASE WHEN status = 'running' THEN heartbeat_at_ms ELSE NULL END,\n CASE WHEN status = 'running' THEN heartbeat_at_ms ELSE NULL END,\n claim_host_id,\n claim_pid,\n claim_nonce,\n claimed_at_ms\n FROM flows_runs\n WHERE status = 'running' OR claim_host_id IS NOT NULL\n ON CONFLICT (run_id) DO NOTHING\n `\n})\n"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect-native analogue of Bazel Skyframe's `NotifyingHelper` and
|
|
3
|
+
* `NotifyingInMemoryGraph`: wrap a record-of-Effect-methods service so a listener hook fires around
|
|
4
|
+
* every operation. The hook runs in the calling fiber, so a hook that dies,
|
|
5
|
+
* interrupts, or awaits a `Latch` injects crashes, fence loss, and exact
|
|
6
|
+
* sequencing at any durable transition point, the mechanism that makes
|
|
7
|
+
* interstitial interleaving bugs testable at all.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately NOT ported from Skyframe: `DeterministicHelper`'s
|
|
10
|
+
* deterministic-scheduler machinery, `ChainedFunction`, and `TrackingAwaiter`.
|
|
11
|
+
* Effect already gives us deterministic fibers, `TestClock`, `Latch`, and
|
|
12
|
+
* failure propagation across fiber joins, so those helpers have no residue
|
|
13
|
+
* here.
|
|
14
|
+
*
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
import type * as Context from "effect/Context";
|
|
18
|
+
import * as Effect from "effect/Effect";
|
|
19
|
+
import * as Layer from "effect/Layer";
|
|
20
|
+
/**
|
|
21
|
+
* Whether the hook is firing before or after the delegated operation.
|
|
22
|
+
* Skyframe's `NotifyingHelper.Order`, minus its `BEFORE_AND_AFTER` (pass the
|
|
23
|
+
* same hook body for both firings instead).
|
|
24
|
+
*
|
|
25
|
+
* @category models
|
|
26
|
+
* @since 0.1.0
|
|
27
|
+
*/
|
|
28
|
+
export type Order = "before" | "after";
|
|
29
|
+
/**
|
|
30
|
+
* Listener invoked around every delegated operation. `op` is the service
|
|
31
|
+
* method name (Skyframe's `EventType`, but open-world), `args` the call
|
|
32
|
+
* arguments. Express crash injection as `Effect.die` or `Effect.interrupt`
|
|
33
|
+
* inside the hook, the hook's type stays `Effect<void>` so the wrapped
|
|
34
|
+
* service is structurally identical to the real one.
|
|
35
|
+
*
|
|
36
|
+
* The `after` firing only happens when the operation succeeds; a failed
|
|
37
|
+
* operation already is the interesting event.
|
|
38
|
+
*
|
|
39
|
+
* @category models
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
*/
|
|
42
|
+
export type Hook = (op: string, order: Order, args: ReadonlyArray<unknown>) => Effect.Effect<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Wraps a service so `hook(op, "before" | "after", args)` fires around every
|
|
45
|
+
* Effect-valued operation, delegating to the real implementation in between.
|
|
46
|
+
*
|
|
47
|
+
* Generic over any record-of-Effect-methods service (`Journal`, `CacheStore`,
|
|
48
|
+
* `AttemptStore`, `RunStore`, ...): Effect-returning methods and plain Effect
|
|
49
|
+
* properties are hooked; non-Effect members (e.g. `Stream`-returning methods)
|
|
50
|
+
* delegate untouched.
|
|
51
|
+
*
|
|
52
|
+
* @category constructors
|
|
53
|
+
* @since 0.1.0
|
|
54
|
+
*/
|
|
55
|
+
export declare const wrap: <S extends object>(service: S, hook: Hook) => S;
|
|
56
|
+
/**
|
|
57
|
+
* Layer form of {@link wrap}: reads the real service from the environment and
|
|
58
|
+
* re-provides the notifying wrapper under the same key, so an existing stack
|
|
59
|
+
* gains a listener without touching its construction.
|
|
60
|
+
*
|
|
61
|
+
* @category layers
|
|
62
|
+
* @since 0.1.0
|
|
63
|
+
*/
|
|
64
|
+
export declare const layer: <Id, S extends object>(tag: Context.Key<Id, S>, hook: Hook) => Layer.Layer<Id, never, Id>;
|
|
65
|
+
//# sourceMappingURL=Notifying.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Notifying.d.ts","sourceRoot":"","sources":["../../../src/test/Notifying.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,OAAO,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;GAOG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEtC;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,IAAI,GAAG,CACjB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AAExB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,IAAI,GAAI,CAAC,SAAS,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,IAAI,KAAG,CAyB/D,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,GAAI,EAAE,EAAE,CAAC,SAAS,MAAM,EACxC,KAAK,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,EACvB,MAAM,IAAI,KACT,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,CAAyE,CAAA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect-native analogue of Bazel Skyframe's `NotifyingHelper` and
|
|
3
|
+
* `NotifyingInMemoryGraph`: wrap a record-of-Effect-methods service so a listener hook fires around
|
|
4
|
+
* every operation. The hook runs in the calling fiber, so a hook that dies,
|
|
5
|
+
* interrupts, or awaits a `Latch` injects crashes, fence loss, and exact
|
|
6
|
+
* sequencing at any durable transition point, the mechanism that makes
|
|
7
|
+
* interstitial interleaving bugs testable at all.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately NOT ported from Skyframe: `DeterministicHelper`'s
|
|
10
|
+
* deterministic-scheduler machinery, `ChainedFunction`, and `TrackingAwaiter`.
|
|
11
|
+
* Effect already gives us deterministic fibers, `TestClock`, `Latch`, and
|
|
12
|
+
* failure propagation across fiber joins, so those helpers have no residue
|
|
13
|
+
* here.
|
|
14
|
+
*
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
import * as Effect from "effect/Effect";
|
|
18
|
+
import * as Layer from "effect/Layer";
|
|
19
|
+
/**
|
|
20
|
+
* Wraps a service so `hook(op, "before" | "after", args)` fires around every
|
|
21
|
+
* Effect-valued operation, delegating to the real implementation in between.
|
|
22
|
+
*
|
|
23
|
+
* Generic over any record-of-Effect-methods service (`Journal`, `CacheStore`,
|
|
24
|
+
* `AttemptStore`, `RunStore`, ...): Effect-returning methods and plain Effect
|
|
25
|
+
* properties are hooked; non-Effect members (e.g. `Stream`-returning methods)
|
|
26
|
+
* delegate untouched.
|
|
27
|
+
*
|
|
28
|
+
* @category constructors
|
|
29
|
+
* @since 0.1.0
|
|
30
|
+
*/
|
|
31
|
+
export const wrap = (service, hook) => {
|
|
32
|
+
const wrapped = {};
|
|
33
|
+
for (const key of Object.keys(service)) {
|
|
34
|
+
const value = service[key];
|
|
35
|
+
if (Effect.isEffect(value)) {
|
|
36
|
+
wrapped[key] = hook(key, "before", []).pipe(Effect.andThen(value), Effect.tap(() => hook(key, "after", [])));
|
|
37
|
+
}
|
|
38
|
+
else if (typeof value === "function") {
|
|
39
|
+
const method = value;
|
|
40
|
+
wrapped[key] = (...args) => {
|
|
41
|
+
const result = method.apply(service, args);
|
|
42
|
+
return Effect.isEffect(result)
|
|
43
|
+
? hook(key, "before", args).pipe(Effect.andThen(result), Effect.tap(() => hook(key, "after", args)))
|
|
44
|
+
: result;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
wrapped[key] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return wrapped;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Layer form of {@link wrap}: reads the real service from the environment and
|
|
55
|
+
* re-provides the notifying wrapper under the same key, so an existing stack
|
|
56
|
+
* gains a listener without touching its construction.
|
|
57
|
+
*
|
|
58
|
+
* @category layers
|
|
59
|
+
* @since 0.1.0
|
|
60
|
+
*/
|
|
61
|
+
export const layer = (tag, hook) => Layer.effect(tag)(Effect.map(tag, (service) => wrap(service, hook)));
|
|
62
|
+
//# sourceMappingURL=Notifying.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Notifying.js","sourceRoot":"","sources":["../../../src/test/Notifying.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AA+BrC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAmB,OAAU,EAAE,IAAU,EAAK,EAAE;IAClE,MAAM,OAAO,GAA4B,EAAE,CAAA;IAC3C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,KAAK,GAAI,OAAmC,CAAC,GAAG,CAAC,CAAA;QACvD,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,IAAI,CACzC,MAAM,CAAC,OAAO,CAAC,KAAiD,CAAC,EACjE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,CACzC,CAAA;QACH,CAAC;aAAM,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,KAA6C,CAAA;YAC5D,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,IAAoB,EAAE,EAAE;gBACzC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;gBAC1C,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;oBAC5B,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,IAAI,CAC9B,MAAM,CAAC,OAAO,CAAC,MAAkD,CAAC,EAClE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAC3C;oBACD,CAAC,CAAC,MAAM,CAAA;YACZ,CAAC,CAAA;QACH,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QACtB,CAAC;IACH,CAAC;IACD,OAAO,OAAY,CAAA;AACrB,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,GAAuB,EACvB,IAAU,EACkB,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA","sourcesContent":["/**\n * Effect-native analogue of Bazel Skyframe's `NotifyingHelper` and\n * `NotifyingInMemoryGraph`: wrap a record-of-Effect-methods service so a listener hook fires around\n * every operation. The hook runs in the calling fiber, so a hook that dies,\n * interrupts, or awaits a `Latch` injects crashes, fence loss, and exact\n * sequencing at any durable transition point, the mechanism that makes\n * interstitial interleaving bugs testable at all.\n *\n * Deliberately NOT ported from Skyframe: `DeterministicHelper`'s\n * deterministic-scheduler machinery, `ChainedFunction`, and `TrackingAwaiter`.\n * Effect already gives us deterministic fibers, `TestClock`, `Latch`, and\n * failure propagation across fiber joins, so those helpers have no residue\n * here.\n *\n * @since 0.1.0\n */\n\nimport type * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\n\n/**\n * Whether the hook is firing before or after the delegated operation.\n * Skyframe's `NotifyingHelper.Order`, minus its `BEFORE_AND_AFTER` (pass the\n * same hook body for both firings instead).\n *\n * @category models\n * @since 0.1.0\n */\nexport type Order = \"before\" | \"after\"\n\n/**\n * Listener invoked around every delegated operation. `op` is the service\n * method name (Skyframe's `EventType`, but open-world), `args` the call\n * arguments. Express crash injection as `Effect.die` or `Effect.interrupt`\n * inside the hook, the hook's type stays `Effect<void>` so the wrapped\n * service is structurally identical to the real one.\n *\n * The `after` firing only happens when the operation succeeds; a failed\n * operation already is the interesting event.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Hook = (\n op: string,\n order: Order,\n args: ReadonlyArray<unknown>\n) => Effect.Effect<void>\n\n/**\n * Wraps a service so `hook(op, \"before\" | \"after\", args)` fires around every\n * Effect-valued operation, delegating to the real implementation in between.\n *\n * Generic over any record-of-Effect-methods service (`Journal`, `CacheStore`,\n * `AttemptStore`, `RunStore`, ...): Effect-returning methods and plain Effect\n * properties are hooked; non-Effect members (e.g. `Stream`-returning methods)\n * delegate untouched.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const wrap = <S extends object>(service: S, hook: Hook): S => {\n const wrapped: Record<string, unknown> = {}\n for (const key of Object.keys(service)) {\n const value = (service as Record<string, unknown>)[key]\n if (Effect.isEffect(value)) {\n wrapped[key] = hook(key, \"before\", []).pipe(\n Effect.andThen(value as Effect.Effect<unknown, unknown, unknown>),\n Effect.tap(() => hook(key, \"after\", []))\n )\n } else if (typeof value === \"function\") {\n const method = value as (...args: Array<unknown>) => unknown\n wrapped[key] = (...args: Array<unknown>) => {\n const result = method.apply(service, args)\n return Effect.isEffect(result)\n ? hook(key, \"before\", args).pipe(\n Effect.andThen(result as Effect.Effect<unknown, unknown, unknown>),\n Effect.tap(() => hook(key, \"after\", args))\n )\n : result\n }\n } else {\n wrapped[key] = value\n }\n }\n return wrapped as S\n}\n\n/**\n * Layer form of {@link wrap}: reads the real service from the environment and\n * re-provides the notifying wrapper under the same key, so an existing stack\n * gains a listener without touching its construction.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer = <Id, S extends object>(\n tag: Context.Key<Id, S>,\n hook: Hook\n): Layer.Layer<Id, never, Id> => Layer.effect(tag)(Effect.map(tag, (service) => wrap(service, hook)))\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic bundle of the production journal service.
|
|
3
|
+
*
|
|
4
|
+
* This provides the journal and nothing else. A consumer that also needs run
|
|
5
|
+
* state or the step cache composes `@smthrs/run-store/test/TestRunStore` and
|
|
6
|
+
* `@smthrs/step-cache/test/TestCacheStore` over the same database, or takes
|
|
7
|
+
* the whole engine bundle from `@smthrs/engine-store/test/TestStores`.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
*/
|
|
11
|
+
import * as Layer from "effect/Layer";
|
|
12
|
+
import * as SqlJournal from "../SqlJournal.ts";
|
|
13
|
+
/**
|
|
14
|
+
* Options for the deterministic journal bundle.
|
|
15
|
+
*
|
|
16
|
+
* The production options with `capacity` and `overflow` made optional. Every
|
|
17
|
+
* field forwards to {@link SqlJournal.layer} unchanged, so a suite can exercise
|
|
18
|
+
* index-bound eviction, the entry byte bound, a custom or noop redactor, and
|
|
19
|
+
* the compaction policy through the bundle instead of hand-assembling the layer
|
|
20
|
+
* stack the bundle exists to hide.
|
|
21
|
+
*
|
|
22
|
+
* The type is derived rather than mirrored because a mirror silently falls
|
|
23
|
+
* behind: the hand-copied list had already lost `maxEntryBytes`, so the one
|
|
24
|
+
* production bound a hostile-payload suite needs was unreachable here.
|
|
25
|
+
*
|
|
26
|
+
* @category models
|
|
27
|
+
* @since 0.1.0
|
|
28
|
+
*/
|
|
29
|
+
export interface TestJournalOptions extends Omit<SqlJournal.SqlJournalOptions, "capacity" | "overflow"> {
|
|
30
|
+
/** Admission-queue bound and `changes` buffer size. Defaults to 1024. */
|
|
31
|
+
readonly capacity?: number | undefined;
|
|
32
|
+
/** Policy applied when the admission queue is full. Defaults to `reject`. */
|
|
33
|
+
readonly overflow?: SqlJournal.SqlJournalOptions["overflow"] | undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Provides the production SQLite journal over an in-memory database.
|
|
37
|
+
* Migrations run before the journal is exposed.
|
|
38
|
+
*
|
|
39
|
+
* @category layers
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
*/
|
|
42
|
+
export declare const layer: (options?: TestJournalOptions) => Layer.Layer<import("../Journal.ts").Journal, import("effect/unstable/sql/SqlError").SqlError | import("../Journal.ts").JournalError | import("effect/unstable/sql/Migrator").MigrationError, never>;
|
|
43
|
+
//# sourceMappingURL=TestJournal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TestJournal.d.ts","sourceRoot":"","sources":["../../../src/test/TestJournal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAE9C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,UAAU,CAAC,iBAAiB,EAAE,UAAU,GAAG,UAAU,CAAC;IACrG,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,UAAU,CAAC,GAAG,SAAS,CAAA;CACzE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,GAAI,UAAU,kBAAkB,wMAKgC,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic bundle of the production journal service.
|
|
3
|
+
*
|
|
4
|
+
* This provides the journal and nothing else. A consumer that also needs run
|
|
5
|
+
* state or the step cache composes `@smthrs/run-store/test/TestRunStore` and
|
|
6
|
+
* `@smthrs/step-cache/test/TestCacheStore` over the same database, or takes
|
|
7
|
+
* the whole engine bundle from `@smthrs/engine-store/test/TestStores`.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
*/
|
|
11
|
+
import * as TestDatabase from "@smthrs/database/test/TestDatabase";
|
|
12
|
+
import * as Layer from "effect/Layer";
|
|
13
|
+
import * as Migrations from "../Migrations.js";
|
|
14
|
+
import * as SqlJournal from "../SqlJournal.js";
|
|
15
|
+
/**
|
|
16
|
+
* Provides the production SQLite journal over an in-memory database.
|
|
17
|
+
* Migrations run before the journal is exposed.
|
|
18
|
+
*
|
|
19
|
+
* @category layers
|
|
20
|
+
* @since 0.1.0
|
|
21
|
+
*/
|
|
22
|
+
export const layer = (options) => SqlJournal.layer({
|
|
23
|
+
...options,
|
|
24
|
+
capacity: options?.capacity ?? 1024,
|
|
25
|
+
overflow: options?.overflow ?? "reject"
|
|
26
|
+
}).pipe(Layer.provide(Layer.provideMerge(Migrations.layer, TestDatabase.layer)));
|
|
27
|
+
//# sourceMappingURL=TestJournal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TestJournal.js","sourceRoot":"","sources":["../../../src/test/TestJournal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,YAAY,MAAM,oCAAoC,CAAA;AAClE,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAyB9C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,OAA4B,EAAE,EAAE,CACpD,UAAU,CAAC,KAAK,CAAC;IACf,GAAG,OAAO;IACV,QAAQ,EAAE,OAAO,EAAE,QAAQ,IAAI,IAAI;IACnC,QAAQ,EAAE,OAAO,EAAE,QAAQ,IAAI,QAAQ;CACxC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA","sourcesContent":["/**\n * Deterministic bundle of the production journal service.\n *\n * This provides the journal and nothing else. A consumer that also needs run\n * state or the step cache composes `@smthrs/run-store/test/TestRunStore` and\n * `@smthrs/step-cache/test/TestCacheStore` over the same database, or takes\n * the whole engine bundle from `@smthrs/engine-store/test/TestStores`.\n *\n * @since 0.1.0\n */\n\nimport * as TestDatabase from \"@smthrs/database/test/TestDatabase\"\nimport * as Layer from \"effect/Layer\"\nimport * as Migrations from \"../Migrations.ts\"\nimport * as SqlJournal from \"../SqlJournal.ts\"\n\n/**\n * Options for the deterministic journal bundle.\n *\n * The production options with `capacity` and `overflow` made optional. Every\n * field forwards to {@link SqlJournal.layer} unchanged, so a suite can exercise\n * index-bound eviction, the entry byte bound, a custom or noop redactor, and\n * the compaction policy through the bundle instead of hand-assembling the layer\n * stack the bundle exists to hide.\n *\n * The type is derived rather than mirrored because a mirror silently falls\n * behind: the hand-copied list had already lost `maxEntryBytes`, so the one\n * production bound a hostile-payload suite needs was unreachable here.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface TestJournalOptions extends Omit<SqlJournal.SqlJournalOptions, \"capacity\" | \"overflow\"> {\n /** Admission-queue bound and `changes` buffer size. Defaults to 1024. */\n readonly capacity?: number | undefined\n /** Policy applied when the admission queue is full. Defaults to `reject`. */\n readonly overflow?: SqlJournal.SqlJournalOptions[\"overflow\"] | undefined\n}\n\n/**\n * Provides the production SQLite journal over an in-memory database.\n * Migrations run before the journal is exposed.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer = (options?: TestJournalOptions) =>\n SqlJournal.layer({\n ...options,\n capacity: options?.capacity ?? 1024,\n overflow: options?.overflow ?? \"reject\"\n }).pipe(Layer.provide(Layer.provideMerge(Migrations.layer, TestDatabase.layer)))\n"]}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "@smthrs/journal"
|
|
3
|
+
description: "An append-only event history for Effect applications: a lossless lifecycle channel beside a lossy telemetry channel, credentials scrubbed before anything is persisted, and an owner fence that refuses a write from a process that lost the run."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`@smthrs/journal` records what a long-running piece of work did, in order, in
|
|
7
|
+
SQLite. Rows are appended and never updated, so the history reads the same on
|
|
8
|
+
the tenth pass as on the first, and a process that dies halfway through leaves
|
|
9
|
+
behind everything it had already committed.
|
|
10
|
+
|
|
11
|
+
It is a set of [Effect](https://effect.website) services: every write returns a
|
|
12
|
+
typed receipt or a typed failure, and the database arrives as a layer you
|
|
13
|
+
compose. It belongs to the Smithers durable flow engine, but it runs on its
|
|
14
|
+
own: no other Smithers package is required.
|
|
15
|
+
|
|
16
|
+
Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
|
|
17
|
+
|
|
18
|
+
`effect` is a peer dependency at exactly `4.0.0-rc.115`. Two copies of `effect`
|
|
19
|
+
in one program are two sets of service tags, so a journal layer built against
|
|
20
|
+
one copy cannot be provided to a program holding the other.
|
|
21
|
+
|
|
22
|
+
## What it solves
|
|
23
|
+
|
|
24
|
+
Application logs are the usual answer to "what happened here", and they stop
|
|
25
|
+
being an answer the moment another program has to read them. A journal entry is
|
|
26
|
+
a typed record with a sequence number, so a supervisor, a resumed run, or a UI
|
|
27
|
+
can replay the history instead of parsing it.
|
|
28
|
+
|
|
29
|
+
- **Two channels, one order.** A lifecycle event ("the run finished") is on
|
|
30
|
+
disk before its call returns. A telemetry event ("40 percent compiled") goes
|
|
31
|
+
through a bounded queue that drops under pressure rather than stalling the
|
|
32
|
+
writer. Both land in one per-run sequence, so the cheap channel cannot
|
|
33
|
+
reorder the expensive one.
|
|
34
|
+
- **Every write gets an answer.** An emission returns `Accepted`, `Duplicate`,
|
|
35
|
+
or `Dropped`, and every failure carries one of fourteen stable codes. A
|
|
36
|
+
caller never has to guess whether an entry survived.
|
|
37
|
+
- **A retry is not a second event.** Producer identity is
|
|
38
|
+
`(runId, sourceId, sourceSeq)`, unique in the database, so a producer that
|
|
39
|
+
replays after a crash gets `Duplicate` back rather than doubling its history.
|
|
40
|
+
- **Credentials never reach the file.** Payloads are scrubbed on the write
|
|
41
|
+
path, before encoding, because a row is permanent and gets replayed verbatim
|
|
42
|
+
to every later reader.
|
|
43
|
+
- **A stale process cannot append.** The durable channel takes an owner token
|
|
44
|
+
and commits only while the database still records that owner as the run's
|
|
45
|
+
owner. A process that was replaced fails with `fence_lost` instead of writing
|
|
46
|
+
into a history it no longer owns.
|
|
47
|
+
- **State and its entry commit together.** `transact` puts your own writes and
|
|
48
|
+
the entry that describes them in one transaction, so the two can never
|
|
49
|
+
disagree about what happened.
|
|
50
|
+
- **A long run stops growing.** A checkpoint pins replay state to a sequence,
|
|
51
|
+
and compaction deletes the entries below it.
|
|
52
|
+
|
|
53
|
+
## One run, both channels
|
|
54
|
+
|
|
55
|
+
This program writes a lifecycle event and a telemetry event to the same run,
|
|
56
|
+
then reads the run back. It needs Node.js 26.4.0 or later, where the built-in
|
|
57
|
+
SQLite module lives, and it writes a real `history.db` in the working
|
|
58
|
+
directory:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import * as DurableWriter from "@smthrs/database/DurableWriter"
|
|
62
|
+
import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"
|
|
63
|
+
import { Journal, JournalEvent, Migrations, SqlJournal } from "@smthrs/journal"
|
|
64
|
+
import * as Effect from "effect/Effect"
|
|
65
|
+
import * as Layer from "effect/Layer"
|
|
66
|
+
|
|
67
|
+
const database = Layer.provideMerge(
|
|
68
|
+
DurableWriter.layer(),
|
|
69
|
+
NodeDatabase.layer({ filename: "history.db" })
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const journalLayer = SqlJournal.layer({ capacity: 1024, overflow: "reject" }).pipe(
|
|
73
|
+
Layer.provide(Layer.provideMerge(Migrations.layer, database))
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const runId = "run-1" as JournalEvent.RunId
|
|
77
|
+
const sourceId = "engine" as JournalEvent.SourceId
|
|
78
|
+
|
|
79
|
+
const program = Effect.gen(function*() {
|
|
80
|
+
const journal = yield* Journal.Journal
|
|
81
|
+
|
|
82
|
+
const created = yield* journal.emitDurableUnfenced({
|
|
83
|
+
runId,
|
|
84
|
+
sourceId,
|
|
85
|
+
eventType: "run.created",
|
|
86
|
+
payload: { flow: "build", token: "ghp_0123456789abcdefghijklmnopqrstuvwxyz" }
|
|
87
|
+
})
|
|
88
|
+
console.log("durable:", created._tag, "seq", created.seq)
|
|
89
|
+
|
|
90
|
+
yield* journal.emitLossy({
|
|
91
|
+
runId,
|
|
92
|
+
sourceId,
|
|
93
|
+
eventType: "step.progress",
|
|
94
|
+
payload: { step: "compile", percent: 40 }
|
|
95
|
+
})
|
|
96
|
+
yield* journal.flush
|
|
97
|
+
|
|
98
|
+
const page = yield* journal.entries({ runId, limit: 100 })
|
|
99
|
+
for (const entry of page.entries) {
|
|
100
|
+
console.log(entry.seq, entry.eventType, JSON.stringify(entry.payload))
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
await Effect.runPromise(program.pipe(Effect.provide(journalLayer), Effect.orDie))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
durable: Accepted seq 0
|
|
109
|
+
0 run.created {"flow":"build","token":"[REDACTED]"}
|
|
110
|
+
1 step.progress {"percent":40,"step":"compile"}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The token came back as `[REDACTED]` because it was scrubbed before the row was
|
|
114
|
+
encoded, so the credential is not in `history.db` and never was. The two
|
|
115
|
+
entries share one `seq` line because both channels write one history. The
|
|
116
|
+
`flush` is the barrier for the lossy channel only: the durable entry was
|
|
117
|
+
already committed when its receipt arrived.
|
|
118
|
+
|
|
119
|
+
## Where this sits
|
|
120
|
+
|
|
121
|
+
The journal owns exactly the history and nothing else.
|
|
122
|
+
[`@smthrs/flows`](/api/flows) is the engine's barrel: it re-exports this
|
|
123
|
+
package as a namespace next to the flow language, the stores, and the runtime
|
|
124
|
+
that wires them over one SQLite file, so
|
|
125
|
+
`import { Journal } from "@smthrs/flows"` reaches the same code as
|
|
126
|
+
`import * as Journal from "@smthrs/journal"`. Depend on the barrel when you
|
|
127
|
+
want a working engine, and on this package when the event history is the part
|
|
128
|
+
you need: an audit trail, a live feed for a UI, or the replay log of a system
|
|
129
|
+
you are building yourself.
|
|
130
|
+
|
|
131
|
+
The neighboring stores hold what the journal deliberately does not.
|
|
132
|
+
Run and attempt state live in [`@smthrs/run-store`](/api/run-store), sealed
|
|
133
|
+
step results in [`@smthrs/step-cache`](/api/step-cache), and the durable
|
|
134
|
+
deferred and clock tables in [`@smthrs/engine-store`](/api/engine-store), which
|
|
135
|
+
composes all of them. Underneath every one of them,
|
|
136
|
+
[`@smthrs/database`](/api/database) is the single write boundary they share,
|
|
137
|
+
which is what makes `transact` possible.
|
|
138
|
+
|
|
139
|
+
Above the whole engine sits the
|
|
140
|
+
[`smithers` command-line interface](/api/cli), which runs flows out of a
|
|
141
|
+
project directory without your composing any of this by hand.
|
|
142
|
+
|
|
143
|
+
One name collides. [`@smthrs/chain`](/api/chain) also has a journal: an
|
|
144
|
+
in-process event array that is a single chain's only state. It is a different
|
|
145
|
+
object with a different contract, described in
|
|
146
|
+
[the chain journal](/pkg/chain/concepts/journal).
|
|
147
|
+
|
|
148
|
+
## Next steps
|
|
149
|
+
|
|
150
|
+
- [Installation](./installation.md): the database layer, the migration sets a
|
|
151
|
+
journal needs, and which extra table the fenced write path reads.
|
|
152
|
+
- [Quickstart](./quickstart.md): the program above built one step at a time,
|
|
153
|
+
with what each line of output proves.
|
|
154
|
+
- [The two channels](./concepts/two-channels.md): which write to reach for,
|
|
155
|
+
what each receipt means, and why sequence gaps are valid.
|
|
156
|
+
- [The owner fence](./concepts/owner-fence.md): why the durable channel takes
|
|
157
|
+
an owner, and when the unfenced one is correct.
|
|
158
|
+
- [Read and follow a run](./guides/read-a-run.md): paging, the lossless
|
|
159
|
+
follower, and the buffer that drops for a slow subscriber.
|
|
160
|
+
- [Test against a real journal](./guides/testing.md): the production layer over
|
|
161
|
+
an in-memory database, and injecting a crash at an exact transition.
|
|
162
|
+
- [API reference](./api.md): every public export, grouped by namespace.
|
|
163
|
+
- [Troubleshooting](./troubleshooting.md): every error code, what causes it,
|
|
164
|
+
and what to change.
|