@effect-agent/storage-sqlite 0.0.1-beta.4 → 0.0.1-beta.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-agent/storage-sqlite",
3
- "version": "0.0.1-beta.4",
3
+ "version": "0.0.1-beta.5",
4
4
  "description": "Node SQLite Conversation store and submission ledger adapters for the Effect Agent durable runtime.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,13 +28,13 @@
28
28
  "test": "vp test --passWithNoTests"
29
29
  },
30
30
  "dependencies": {
31
- "@effect-agent/session": "0.0.1-beta.4",
32
- "@effect/platform-node": "4.0.0-beta.102",
33
- "@effect/sql-sqlite-node": "4.0.0-beta.102",
34
- "effect": "4.0.0-beta.102"
31
+ "@effect-agent/session": "0.0.1-beta.5",
32
+ "@effect/platform-node": "4.0.0-beta.107",
33
+ "@effect/sql-sqlite-node": "4.0.0-beta.107",
34
+ "effect": "4.0.0-beta.107"
35
35
  },
36
36
  "devDependencies": {
37
- "@effect/vitest": "4.0.0-beta.102",
37
+ "@effect/vitest": "4.0.0-beta.107",
38
38
  "typescript": "7.0.2",
39
39
  "vite-plus": "0.2.6"
40
40
  }
package/src/errors.ts CHANGED
@@ -2,7 +2,7 @@ import { CanonicalSequence, ProducerEpoch } from "@effect-agent/session";
2
2
  import { Schema } from "effect";
3
3
 
4
4
  /** The SQLite file uses a private-development format this adapter cannot read. */
5
- export class SqliteStorageCompatibilityError extends Schema.TaggedErrorClass<SqliteStorageCompatibilityError>()(
5
+ export class SqliteStorageCompatibilityError extends Schema.TaggedError<SqliteStorageCompatibilityError>()(
6
6
  "SqliteStorageCompatibilityError",
7
7
  {
8
8
  actualVersion: Schema.Int,
@@ -12,7 +12,7 @@ export class SqliteStorageCompatibilityError extends Schema.TaggedErrorClass<Sql
12
12
  ) {}
13
13
 
14
14
  /** Stored bytes failed the current Schema and cannot be used as recovery truth. */
15
- export class SqliteStorageCorruptionError extends Schema.TaggedErrorClass<SqliteStorageCorruptionError>()(
15
+ export class SqliteStorageCorruptionError extends Schema.TaggedError<SqliteStorageCorruptionError>()(
16
16
  "SqliteStorageCorruptionError",
17
17
  {
18
18
  message: Schema.String,
@@ -22,7 +22,7 @@ export class SqliteStorageCorruptionError extends Schema.TaggedErrorClass<Sqlite
22
22
  ) {}
23
23
 
24
24
  /** SQLite infrastructure failed while opening or operating the store. */
25
- export class SqliteStorageError extends Schema.TaggedErrorClass<SqliteStorageError>()(
25
+ export class SqliteStorageError extends Schema.TaggedError<SqliteStorageError>()(
26
26
  "SqliteStorageError",
27
27
  {
28
28
  cause: Schema.optionalKey(Schema.Defect()),
@@ -36,7 +36,7 @@ export class SqliteStorageError extends Schema.TaggedErrorClass<SqliteStorageErr
36
36
  * SubmissionLedger port as the typed `LedgerError` with this error preserved as its cause,
37
37
  * so the adapter-level tag is never erased.
38
38
  */
39
- export class SqliteLedgerError extends Schema.TaggedErrorClass<SqliteLedgerError>()(
39
+ export class SqliteLedgerError extends Schema.TaggedError<SqliteLedgerError>()(
40
40
  "SqliteLedgerError",
41
41
  {
42
42
  cause: Schema.optionalKey(Schema.Defect()),
@@ -49,7 +49,7 @@ export class SqliteLedgerError extends Schema.TaggedErrorClass<SqliteLedgerError
49
49
  * A canonical batch retry conflicts with existing append state. Tail conflicts carry the
50
50
  * actual committed tail as a diagnostic resume hint.
51
51
  */
52
- export class SqliteAppendConflict extends Schema.TaggedErrorClass<SqliteAppendConflict>()(
52
+ export class SqliteAppendConflict extends Schema.TaggedError<SqliteAppendConflict>()(
53
53
  "SqliteAppendConflict",
54
54
  {
55
55
  message: Schema.String,
@@ -64,7 +64,7 @@ export class SqliteAppendConflict extends Schema.TaggedErrorClass<SqliteAppendCo
64
64
  * require the exact registered epoch, so both older and newer unregistered epochs are fenced;
65
65
  * a newer epoch takes over by materializing first.
66
66
  */
67
- export class SqliteFenceRejected extends Schema.TaggedErrorClass<SqliteFenceRejected>()(
67
+ export class SqliteFenceRejected extends Schema.TaggedError<SqliteFenceRejected>()(
68
68
  "SqliteFenceRejected",
69
69
  {
70
70
  actualEpoch: ProducerEpoch,
@@ -78,7 +78,7 @@ export class SqliteFenceRejected extends Schema.TaggedErrorClass<SqliteFenceReje
78
78
  * timeout (SQLITE_BUSY / SQLITE_LOCKED). Another transiently coexisting owner is writing;
79
79
  * the operation did not mutate canonical state and is safe to retry.
80
80
  */
81
- export class SqliteWriteContention extends Schema.TaggedErrorClass<SqliteWriteContention>()(
81
+ export class SqliteWriteContention extends Schema.TaggedError<SqliteWriteContention>()(
82
82
  "SqliteWriteContention",
83
83
  {
84
84
  cause: Schema.optionalKey(Schema.Defect()),
@@ -88,7 +88,7 @@ export class SqliteWriteContention extends Schema.TaggedErrorClass<SqliteWriteCo
88
88
  ) {}
89
89
 
90
90
  /** A checkpoint conflicts with a previously stored checkpoint at the same offset. */
91
- export class SqliteCheckpointConflict extends Schema.TaggedErrorClass<SqliteCheckpointConflict>()(
91
+ export class SqliteCheckpointConflict extends Schema.TaggedError<SqliteCheckpointConflict>()(
92
92
  "SqliteCheckpointConflict",
93
93
  {
94
94
  message: Schema.String,
@@ -152,7 +152,7 @@ export const SqliteStorageFailpointLocation = Schema.Literals([
152
152
  export type SqliteStorageFailpointLocation = typeof SqliteStorageFailpointLocation.Type;
153
153
 
154
154
  /** Deterministic test-only fault or pause injected at a SQLite operation boundary. */
155
- export class SqliteStorageFailpointError extends Schema.TaggedErrorClass<SqliteStorageFailpointError>()(
155
+ export class SqliteStorageFailpointError extends Schema.TaggedError<SqliteStorageFailpointError>()(
156
156
  "SqliteStorageFailpointError",
157
157
  {
158
158
  location: SqliteStorageFailpointLocation,
@@ -377,6 +377,39 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
377
377
  ),
378
378
  );
379
379
 
380
+ /**
381
+ * Runs a read-only snapshot under a deferred transaction. Effect's SQLite client now
382
+ * starts every writable-client `withTransaction` with `BEGIN IMMEDIATE`, which is the
383
+ * right default for mutations but would make exports take the write lock and block a
384
+ * concurrent append. Reserving the connection and beginning explicitly preserves the
385
+ * adapter's snapshot-with-concurrent-writer contract. As with the write helper, a failed
386
+ * `BEGIN` is reported directly because there is no transaction to roll back.
387
+ */
388
+ const withReadTransaction =
389
+ (operation: string) =>
390
+ <A, E>(effect: Effect.Effect<A, E>): Effect.Effect<A, E | SqliteStorageError> =>
391
+ Effect.uninterruptibleMask((restore) =>
392
+ Effect.scoped(
393
+ Effect.gen(function* () {
394
+ const connection = yield* sql.reserve.pipe(Effect.mapError(storageError(operation)));
395
+ yield* connection
396
+ .executeUnprepared("BEGIN", [], undefined)
397
+ .pipe(Effect.mapError(storageError(operation)));
398
+ const exit = yield* restore(
399
+ Effect.provideService(effect, sql.transactionService, [connection, 0] as const),
400
+ ).pipe(Effect.exit);
401
+ if (Exit.isSuccess(exit)) {
402
+ yield* connection
403
+ .executeUnprepared("COMMIT", [], undefined)
404
+ .pipe(Effect.mapError(storageError(operation)));
405
+ return exit.value;
406
+ }
407
+ yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], undefined));
408
+ return yield* exit;
409
+ }),
410
+ ).pipe(Effect.withSpan("SqliteJournal.withReadTransaction", { attributes: { operation } })),
411
+ );
412
+
380
413
  const materialize = Effect.fn("SqliteJournal.materialize")(function* (
381
414
  conversationId: string,
382
415
  createdAt: string,
@@ -733,10 +766,9 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
733
766
  const exportConversation = Effect.fn("SqliteJournal.exportConversation")(function* (
734
767
  conversationId: string,
735
768
  ) {
736
- return yield* sql
737
- .withTransaction(
738
- Effect.gen(function* () {
739
- const conversationRows = yield* sql<Record<string, unknown>>`
769
+ return yield* withReadTransaction("export transaction")(
770
+ Effect.gen(function* () {
771
+ const conversationRows = yield* sql<Record<string, unknown>>`
740
772
  SELECT
741
773
  conversation_id,
742
774
  created_at,
@@ -746,14 +778,14 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
746
778
  FROM effect_agent_conversations
747
779
  WHERE conversation_id = ${conversationId}
748
780
  `.pipe(Effect.mapError(storageError("export conversation")));
749
- const conversation = yield* decodeSingleRow(
750
- Schema.Array(ConversationRow),
751
- "effect_agent_conversations",
752
- conversationId,
753
- conversationRows,
754
- );
755
- yield* failpoint("export:after-conversation-read");
756
- const batchRows = yield* sql<Record<string, unknown>>`
781
+ const conversation = yield* decodeSingleRow(
782
+ Schema.Array(ConversationRow),
783
+ "effect_agent_conversations",
784
+ conversationId,
785
+ conversationRows,
786
+ );
787
+ yield* failpoint("export:after-conversation-read");
788
+ const batchRows = yield* sql<Record<string, unknown>>`
757
789
  SELECT
758
790
  conversation_id,
759
791
  batch_id,
@@ -766,7 +798,7 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
766
798
  WHERE conversation_id = ${conversationId}
767
799
  ORDER BY first_sequence
768
800
  `.pipe(Effect.mapError(storageError("export canonical batches")));
769
- const recordRows = yield* sql<Record<string, unknown>>`
801
+ const recordRows = yield* sql<Record<string, unknown>>`
770
802
  SELECT
771
803
  conversation_id,
772
804
  sequence,
@@ -777,7 +809,7 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
777
809
  WHERE conversation_id = ${conversationId}
778
810
  ORDER BY sequence
779
811
  `.pipe(Effect.mapError(storageError("export canonical records")));
780
- const checkpointRows = yield* sql<Record<string, unknown>>`
812
+ const checkpointRows = yield* sql<Record<string, unknown>>`
781
813
  SELECT
782
814
  conversation_id,
783
815
  through_sequence,
@@ -788,34 +820,29 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
788
820
  ORDER BY through_sequence
789
821
  `.pipe(Effect.mapError(storageError("export checkpoints")));
790
822
 
791
- return RawConversationExport.make({
792
- conversation,
793
- batches: yield* decodeRows(
794
- Schema.Array(BatchRow),
795
- "effect_agent_canonical_batches",
796
- conversationId,
797
- batchRows,
798
- ),
799
- records: yield* decodeRows(
800
- Schema.Array(RecordRow),
801
- "effect_agent_canonical_records",
802
- conversationId,
803
- recordRows,
804
- ),
805
- checkpoints: yield* decodeRows(
806
- Schema.Array(CheckpointRow),
807
- "effect_agent_checkpoints",
808
- conversationId,
809
- checkpointRows,
810
- ),
811
- });
812
- }),
813
- )
814
- .pipe(
815
- Effect.catchTag("SqlError", (error) =>
816
- Effect.fail(storageError("export transaction")(error)),
817
- ),
818
- );
823
+ return RawConversationExport.make({
824
+ conversation,
825
+ batches: yield* decodeRows(
826
+ Schema.Array(BatchRow),
827
+ "effect_agent_canonical_batches",
828
+ conversationId,
829
+ batchRows,
830
+ ),
831
+ records: yield* decodeRows(
832
+ Schema.Array(RecordRow),
833
+ "effect_agent_canonical_records",
834
+ conversationId,
835
+ recordRows,
836
+ ),
837
+ checkpoints: yield* decodeRows(
838
+ Schema.Array(CheckpointRow),
839
+ "effect_agent_checkpoints",
840
+ conversationId,
841
+ checkpointRows,
842
+ ),
843
+ });
844
+ }),
845
+ );
819
846
  });
820
847
 
821
848
  const saveCheckpoint = Effect.fn("SqliteJournal.saveCheckpoint")(function* (
@@ -967,10 +994,9 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
967
994
  });
968
995
 
969
996
  const scanStoredPayloads = Effect.fn("SqliteJournal.scanStoredPayloads")(function* () {
970
- return yield* sql
971
- .withTransaction(
972
- Effect.gen(function* () {
973
- const conversations = yield* sql<Record<string, unknown>>`
997
+ return yield* withReadTransaction("startup scan transaction")(
998
+ Effect.gen(function* () {
999
+ const conversations = yield* sql<Record<string, unknown>>`
974
1000
  SELECT
975
1001
  conversation_id,
976
1002
  created_at,
@@ -980,7 +1006,7 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
980
1006
  FROM effect_agent_conversations
981
1007
  ORDER BY conversation_id
982
1008
  `.pipe(Effect.mapError(storageError("scan conversations")));
983
- const batches = yield* sql<Record<string, unknown>>`
1009
+ const batches = yield* sql<Record<string, unknown>>`
984
1010
  SELECT
985
1011
  conversation_id,
986
1012
  batch_id,
@@ -992,7 +1018,7 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
992
1018
  FROM effect_agent_canonical_batches
993
1019
  ORDER BY conversation_id, first_sequence
994
1020
  `.pipe(Effect.mapError(storageError("scan canonical batches")));
995
- const records = yield* sql<Record<string, unknown>>`
1021
+ const records = yield* sql<Record<string, unknown>>`
996
1022
  SELECT
997
1023
  conversation_id,
998
1024
  sequence,
@@ -1002,7 +1028,7 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
1002
1028
  FROM effect_agent_canonical_records
1003
1029
  ORDER BY conversation_id, sequence
1004
1030
  `.pipe(Effect.mapError(storageError("scan canonical records")));
1005
- const checkpoints = yield* sql<Record<string, unknown>>`
1031
+ const checkpoints = yield* sql<Record<string, unknown>>`
1006
1032
  SELECT
1007
1033
  conversation_id,
1008
1034
  through_sequence,
@@ -1011,39 +1037,34 @@ const makeJournal = (sql: SqlClient.SqlClient, failpoint: SqliteJournalFailpoint
1011
1037
  FROM effect_agent_checkpoints
1012
1038
  ORDER BY conversation_id, through_sequence
1013
1039
  `.pipe(Effect.mapError(storageError("scan checkpoints")));
1014
- return {
1015
- conversations: yield* decodeRows(
1016
- Schema.Array(ConversationRow),
1017
- "effect_agent_conversations",
1018
- "startup_scan",
1019
- conversations,
1020
- ),
1021
- batches: yield* decodeRows(
1022
- Schema.Array(BatchRow),
1023
- "effect_agent_canonical_batches",
1024
- "startup_scan",
1025
- batches,
1026
- ),
1027
- records: yield* decodeRows(
1028
- Schema.Array(RecordRow),
1029
- "effect_agent_canonical_records",
1030
- "startup_scan",
1031
- records,
1032
- ),
1033
- checkpoints: yield* decodeRows(
1034
- Schema.Array(CheckpointRow),
1035
- "effect_agent_checkpoints",
1036
- "startup_scan",
1037
- checkpoints,
1038
- ),
1039
- };
1040
- }),
1041
- )
1042
- .pipe(
1043
- Effect.catchTag("SqlError", (error) =>
1044
- Effect.fail(storageError("startup scan transaction")(error)),
1045
- ),
1046
- );
1040
+ return {
1041
+ conversations: yield* decodeRows(
1042
+ Schema.Array(ConversationRow),
1043
+ "effect_agent_conversations",
1044
+ "startup_scan",
1045
+ conversations,
1046
+ ),
1047
+ batches: yield* decodeRows(
1048
+ Schema.Array(BatchRow),
1049
+ "effect_agent_canonical_batches",
1050
+ "startup_scan",
1051
+ batches,
1052
+ ),
1053
+ records: yield* decodeRows(
1054
+ Schema.Array(RecordRow),
1055
+ "effect_agent_canonical_records",
1056
+ "startup_scan",
1057
+ records,
1058
+ ),
1059
+ checkpoints: yield* decodeRows(
1060
+ Schema.Array(CheckpointRow),
1061
+ "effect_agent_checkpoints",
1062
+ "startup_scan",
1063
+ checkpoints,
1064
+ ),
1065
+ };
1066
+ }),
1067
+ );
1047
1068
  });
1048
1069
 
1049
1070
  return {