@effect-agent/storage-sqlite 0.0.1-beta.4 → 0.0.1-beta.6
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/dist/index.mjs +32 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/errors.ts +9 -9
- package/src/sqlite-journal.ts +103 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-agent/storage-sqlite",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.6",
|
|
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.
|
|
32
|
-
"@effect/platform-node": "4.0.0-beta.
|
|
33
|
-
"@effect/sql-sqlite-node": "4.0.0-beta.
|
|
34
|
-
"effect": "4.0.0-beta.
|
|
31
|
+
"@effect-agent/session": "0.0.1-beta.6",
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
155
|
+
export class SqliteStorageFailpointError extends Schema.TaggedError<SqliteStorageFailpointError>()(
|
|
156
156
|
"SqliteStorageFailpointError",
|
|
157
157
|
{
|
|
158
158
|
location: SqliteStorageFailpointLocation,
|
package/src/sqlite-journal.ts
CHANGED
|
@@ -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*
|
|
737
|
-
.
|
|
738
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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*
|
|
971
|
-
.
|
|
972
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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 {
|