@yielded/agent-storage-cloudflare 0.1.0-beta.169 → 0.1.0-beta.171
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/DoMessageDeliveryStore.mjs +1 -1
- package/dist/{DoStorageVersion-DTFoLglB.d.mts → DoStorageVersion-NKnQU3eR.d.mts} +2 -2
- package/dist/DoStorageVersion.d.mts +1 -1
- package/dist/DoStorageVersion.mjs +2 -2
- package/dist/DoSubmissionLedger.mjs +2 -2
- package/dist/DoSubscriptionStore.mjs +74 -58
- package/dist/DoSubscriptionStore.mjs.map +1 -1
- package/dist/{DoThreadStore-CwBvTLoq.mjs → DoThreadStore-CwEDo9Up.mjs} +4 -4
- package/dist/{DoThreadStore-CwBvTLoq.mjs.map → DoThreadStore-CwEDo9Up.mjs.map} +1 -1
- package/dist/DoThreadStore.mjs +1 -1
- package/dist/PortProtocol.d.mts +7396 -7532
- package/dist/{do-journal-Cawnsetj.mjs → do-journal-_MdLGTd3.mjs} +2 -5
- package/dist/do-journal-_MdLGTd3.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{migrations-D_EP3KYf.mjs → migrations-C3N9GN6A.mjs} +50 -16
- package/dist/migrations-C3N9GN6A.mjs.map +1 -0
- package/package.json +1 -1
- package/src/DoSubscriptionStore.ts +124 -86
- package/src/internal/do-journal.ts +0 -6
- package/src/internal/migrations.ts +95 -26
- package/dist/do-journal-Cawnsetj.mjs.map +0 -1
- package/dist/migrations-D_EP3KYf.mjs.map +0 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
backfillSubscriptionSettlementObservation,
|
|
3
|
+
makeSqlSubscriptionStore,
|
|
4
|
+
} from "@yielded/agent-storage-sql/sql-subscription-store";
|
|
5
|
+
import { sameSqliteLayoutStatement } from "@yielded/agent-storage-sql/sqlite-layout-inspection";
|
|
2
6
|
import {
|
|
3
7
|
SourcePartition,
|
|
4
8
|
SubscriptionError,
|
|
@@ -10,7 +14,7 @@ import { Context, Effect, Layer, Schema } from "effect";
|
|
|
10
14
|
import * as SqlClientService from "effect/sql/SqlClient";
|
|
11
15
|
import type { SqlError } from "effect/sql/SqlError";
|
|
12
16
|
|
|
13
|
-
const CURRENT_SUBSCRIPTION_STORE_VERSION =
|
|
17
|
+
const CURRENT_SUBSCRIPTION_STORE_VERSION = 4;
|
|
14
18
|
|
|
15
19
|
const ScanRow = Schema.Struct({
|
|
16
20
|
event_scan_cursor: Schema.String,
|
|
@@ -64,99 +68,133 @@ const validate = <A, I>(schema: Schema.Codec<A, I>, value: unknown, code: string
|
|
|
64
68
|
const decodeRows = <A, I>(schema: Schema.Codec<A, I>, rows: unknown, code: string) =>
|
|
65
69
|
Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => corrupt(code)));
|
|
66
70
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const layoutStatements = [
|
|
72
|
+
"CREATE TABLE effect_agent_subscription_store_state ( singleton INTEGER PRIMARY KEY NOT NULL CHECK(singleton=1), storage_version INTEGER NOT NULL, alarm_generation INTEGER NOT NULL )",
|
|
73
|
+
"CREATE TABLE effect_agent_subscription_sequences ( tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, sequence INTEGER NOT NULL, event_scan_cursor TEXT NOT NULL, delivery_scan_cursor TEXT NOT NULL, recovery_scan_cursor INTEGER NOT NULL, PRIMARY KEY (tenant_id, source_address) )",
|
|
74
|
+
"CREATE TABLE effect_agent_subscriptions ( tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL, ordinal INTEGER NOT NULL, source_name TEXT NOT NULL, source_version TEXT NOT NULL, matching_key TEXT NOT NULL, state TEXT NOT NULL, expires_at_millis INTEGER, recovery_at_millis INTEGER, recovery_present INTEGER NOT NULL DEFAULT 0, record_json TEXT NOT NULL, PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id), UNIQUE (tenant_id, source_address, ordinal) )",
|
|
75
|
+
"CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)",
|
|
76
|
+
"CREATE INDEX effect_agent_subscriptions_capacity ON effect_agent_subscriptions (tenant_id, source_address, expires_at_millis) WHERE state IN ('active', 'paused')",
|
|
77
|
+
"CREATE INDEX effect_agent_subscriptions_owner_capacity ON effect_agent_subscriptions (tenant_id, source_address, owner_id, expires_at_millis) WHERE state IN ('active', 'paused')",
|
|
78
|
+
"CREATE INDEX effect_agent_subscriptions_candidates ON effect_agent_subscriptions (tenant_id, source_address, source_name, source_version, matching_key, ordinal)",
|
|
79
|
+
"CREATE INDEX effect_agent_subscriptions_recovery ON effect_agent_subscriptions (tenant_id, source_address, recovery_at_millis, ordinal) WHERE recovery_at_millis IS NOT NULL",
|
|
80
|
+
"CREATE TABLE effect_agent_subscription_events ( tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, event_id TEXT NOT NULL, source_name TEXT NOT NULL, source_version TEXT NOT NULL, matching_key TEXT NOT NULL, payload_digest TEXT NOT NULL, cutoff INTEGER NOT NULL, cursor INTEGER NOT NULL, routing_complete INTEGER NOT NULL, tombstone INTEGER NOT NULL DEFAULT 0, next_attempt_at_millis INTEGER NOT NULL, record_json TEXT NOT NULL, PRIMARY KEY (tenant_id, source_address, event_id) )",
|
|
81
|
+
"CREATE INDEX effect_agent_subscription_events_pending ON effect_agent_subscription_events (tenant_id, source_address, routing_complete, next_attempt_at_millis, event_id)",
|
|
82
|
+
"CREATE TABLE effect_agent_subscription_deliveries ( tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL, event_id TEXT NOT NULL, delivery_key TEXT NOT NULL, state TEXT NOT NULL, next_attempt_at_millis INTEGER NOT NULL, record_json TEXT NOT NULL, observe_settlement INTEGER NOT NULL DEFAULT 0 CHECK (observe_settlement IN (0, 1)), PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id, event_id), UNIQUE (tenant_id, source_address, delivery_key) )",
|
|
83
|
+
"CREATE INDEX effect_agent_subscription_deliveries_pending ON effect_agent_subscription_deliveries (tenant_id, source_address, state, next_attempt_at_millis, delivery_key)",
|
|
84
|
+
"CREATE INDEX effect_agent_subscription_deliveries_registration ON effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, delivery_key)",
|
|
85
|
+
"CREATE INDEX effect_agent_subscription_deliveries_capacity ON effect_agent_subscription_deliveries (tenant_id, source_address, owner_id) WHERE state IN ('selected', 'prepared')",
|
|
86
|
+
"CREATE INDEX effect_agent_subscription_deliveries_observing ON effect_agent_subscription_deliveries (tenant_id, source_address, owner_id) WHERE state='delivered' AND observe_settlement=1",
|
|
87
|
+
] as const;
|
|
88
|
+
|
|
89
|
+
const layoutObjects = layoutStatements.map((statement, index) => {
|
|
90
|
+
const name = /^CREATE (TABLE|INDEX) ([a-z_][a-z_0-9]*)/.exec(statement);
|
|
91
|
+
|
|
92
|
+
if (name === null) throw new Error("Invalid subscription layout statement");
|
|
93
|
+
|
|
94
|
+
return [name[1].toLowerCase(), name[2], index] as const;
|
|
95
|
+
});
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
const addedIndexes = new Set([
|
|
98
|
+
"effect_agent_subscriptions_capacity",
|
|
99
|
+
"effect_agent_subscriptions_owner_capacity",
|
|
100
|
+
"effect_agent_subscription_deliveries_capacity",
|
|
101
|
+
"effect_agent_subscription_deliveries_observing",
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const LayoutObject = Schema.Struct({
|
|
105
|
+
name: Schema.String,
|
|
106
|
+
type: Schema.String,
|
|
107
|
+
sql: Schema.NullOr(Schema.String),
|
|
108
|
+
});
|
|
79
109
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"effect_agent_subscription_events",
|
|
85
|
-
"effect_agent_subscription_deliveries",
|
|
86
|
-
]);
|
|
110
|
+
/** Only the released v3 layout may upgrade; schema, backfill and version publish commit together. */
|
|
111
|
+
const initializeDoSubscriptionStore = Effect.fnUntraced(function* () {
|
|
112
|
+
const sql = (yield* SqlClientService.SqlClient).withoutTransforms();
|
|
113
|
+
const tables = layoutObjects.filter(([type]) => type === "table").map(([, name]) => name);
|
|
87
114
|
|
|
88
|
-
const
|
|
89
|
-
|
|
115
|
+
const readObjects = sql`SELECT name, type, sql FROM sqlite_master
|
|
116
|
+
WHERE name GLOB 'effect_agent_subscription*' OR (type='trigger' AND tbl_name IN ${sql.in(tables)})`.pipe(
|
|
117
|
+
Effect.flatMap((rows) => decodeRows(LayoutObject, rows, "subscription storage layout")),
|
|
90
118
|
);
|
|
91
119
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
event_scan_cursor TEXT NOT NULL, delivery_scan_cursor TEXT NOT NULL, recovery_scan_cursor INTEGER NOT NULL,
|
|
103
|
-
PRIMARY KEY (tenant_id, source_address)
|
|
104
|
-
)`.withoutTransform;
|
|
105
|
-
yield* sql`CREATE TABLE effect_agent_subscriptions (
|
|
106
|
-
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL,
|
|
107
|
-
ordinal INTEGER NOT NULL, source_name TEXT NOT NULL, source_version TEXT NOT NULL, matching_key TEXT NOT NULL,
|
|
108
|
-
state TEXT NOT NULL, expires_at_millis INTEGER, recovery_at_millis INTEGER, recovery_present INTEGER NOT NULL DEFAULT 0, record_json TEXT NOT NULL,
|
|
109
|
-
PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id), UNIQUE (tenant_id, source_address, ordinal)
|
|
110
|
-
)`.withoutTransform;
|
|
111
|
-
yield* sql`CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)`
|
|
112
|
-
.withoutTransform;
|
|
113
|
-
yield* sql`CREATE INDEX effect_agent_subscriptions_candidates ON effect_agent_subscriptions (tenant_id, source_address, source_name, source_version, matching_key, ordinal)`
|
|
114
|
-
.withoutTransform;
|
|
115
|
-
yield* sql`CREATE INDEX effect_agent_subscriptions_recovery ON effect_agent_subscriptions (tenant_id, source_address, recovery_at_millis, ordinal) WHERE recovery_at_millis IS NOT NULL`
|
|
116
|
-
.withoutTransform;
|
|
117
|
-
yield* sql`CREATE TABLE effect_agent_subscription_events (
|
|
118
|
-
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, event_id TEXT NOT NULL, source_name TEXT NOT NULL,
|
|
119
|
-
source_version TEXT NOT NULL, matching_key TEXT NOT NULL, payload_digest TEXT NOT NULL, cutoff INTEGER NOT NULL,
|
|
120
|
-
cursor INTEGER NOT NULL, routing_complete INTEGER NOT NULL, tombstone INTEGER NOT NULL DEFAULT 0, next_attempt_at_millis INTEGER NOT NULL, record_json TEXT NOT NULL,
|
|
121
|
-
PRIMARY KEY (tenant_id, source_address, event_id)
|
|
122
|
-
)`.withoutTransform;
|
|
123
|
-
yield* sql`CREATE INDEX effect_agent_subscription_events_pending ON effect_agent_subscription_events (tenant_id, source_address, routing_complete, next_attempt_at_millis, event_id)`
|
|
124
|
-
.withoutTransform;
|
|
125
|
-
yield* sql`CREATE TABLE effect_agent_subscription_deliveries (
|
|
126
|
-
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL,
|
|
127
|
-
event_id TEXT NOT NULL, delivery_key TEXT NOT NULL, state TEXT NOT NULL, next_attempt_at_millis INTEGER NOT NULL,
|
|
128
|
-
record_json TEXT NOT NULL, PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id, event_id),
|
|
129
|
-
UNIQUE (tenant_id, source_address, delivery_key)
|
|
130
|
-
)`.withoutTransform;
|
|
131
|
-
yield* sql`CREATE INDEX effect_agent_subscription_deliveries_pending ON effect_agent_subscription_deliveries (tenant_id, source_address, state, next_attempt_at_millis, delivery_key)`
|
|
132
|
-
.withoutTransform;
|
|
133
|
-
yield* sql`CREATE INDEX effect_agent_subscription_deliveries_registration ON effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, delivery_key)`
|
|
134
|
-
.withoutTransform;
|
|
135
|
-
yield* sql`INSERT INTO effect_agent_subscription_store_state (singleton, storage_version, alarm_generation)
|
|
136
|
-
VALUES (1, ${CURRENT_SUBSCRIPTION_STORE_VERSION}, 0)`.withoutTransform;
|
|
137
|
-
}),
|
|
120
|
+
const checkLayout = Effect.fnUntraced(function* (
|
|
121
|
+
objects: ReadonlyArray<typeof LayoutObject.Type>,
|
|
122
|
+
version: number,
|
|
123
|
+
) {
|
|
124
|
+
if (
|
|
125
|
+
objects.some(
|
|
126
|
+
({ name, type }) =>
|
|
127
|
+
(type === "table" && !tables.includes(name)) ||
|
|
128
|
+
type === "trigger" ||
|
|
129
|
+
(version === 3 && addedIndexes.has(name)),
|
|
138
130
|
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
131
|
+
)
|
|
132
|
+
return yield* corrupt("ambiguous subscription storage layout");
|
|
133
|
+
for (const [type, name, index] of layoutObjects) {
|
|
134
|
+
if (version === 3 && addedIndexes.has(name)) continue;
|
|
135
|
+
|
|
136
|
+
const expected =
|
|
137
|
+
version === 3
|
|
138
|
+
? layoutStatements[index].replace(
|
|
139
|
+
", observe_settlement INTEGER NOT NULL DEFAULT 0 CHECK (observe_settlement IN (0, 1))",
|
|
140
|
+
"",
|
|
141
|
+
)
|
|
142
|
+
: layoutStatements[index];
|
|
143
|
+
|
|
144
|
+
const actual = objects.find((row) => row.name === name);
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
actual?.type !== type ||
|
|
148
|
+
actual.sql === null ||
|
|
149
|
+
!sameSqliteLayoutStatement(actual.sql, expected)
|
|
150
|
+
)
|
|
151
|
+
return yield* corrupt("incompatible subscription storage layout " + name);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
145
154
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
155
|
+
yield* sql
|
|
156
|
+
.withTransaction(
|
|
157
|
+
Effect.gen(function* () {
|
|
158
|
+
const objects = yield* readObjects;
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
const hasState = objects.some(
|
|
161
|
+
({ name }) => name === "effect_agent_subscription_store_state",
|
|
162
|
+
);
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
if (!hasState) {
|
|
165
|
+
if (objects.length > 0) return yield* corrupt("partial subscription storage");
|
|
166
|
+
for (const statement of layoutStatements) yield* sql.unsafe(statement).withoutTransform;
|
|
167
|
+
yield* sql`INSERT INTO effect_agent_subscription_store_state (singleton, storage_version, alarm_generation)
|
|
168
|
+
VALUES (1, ${CURRENT_SUBSCRIPTION_STORE_VERSION}, 0)`;
|
|
169
|
+
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const state = yield* decodeRows(
|
|
174
|
+
StoreStateRow,
|
|
175
|
+
yield* sql`SELECT storage_version, alarm_generation FROM effect_agent_subscription_store_state`,
|
|
176
|
+
"read subscription storage version",
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (state.length !== 1) return yield* corrupt("invalid subscription storage version row");
|
|
180
|
+
const version = state[0].storage_version;
|
|
181
|
+
|
|
182
|
+
if (version !== 3 && version !== CURRENT_SUBSCRIPTION_STORE_VERSION)
|
|
183
|
+
return yield* corrupt(
|
|
184
|
+
`incompatible subscription storage version ${version}; expected ${CURRENT_SUBSCRIPTION_STORE_VERSION}`,
|
|
185
|
+
);
|
|
186
|
+
yield* checkLayout(objects, version);
|
|
187
|
+
if (version === CURRENT_SUBSCRIPTION_STORE_VERSION) return;
|
|
188
|
+
|
|
189
|
+
yield* sql`ALTER TABLE effect_agent_subscription_deliveries ADD COLUMN observe_settlement INTEGER NOT NULL DEFAULT 0 CHECK (observe_settlement IN (0, 1))`;
|
|
190
|
+
yield* backfillSubscriptionSettlementObservation();
|
|
191
|
+
for (const [, name, index] of layoutObjects)
|
|
192
|
+
if (addedIndexes.has(name)) yield* sql.unsafe(layoutStatements[index]).withoutTransform;
|
|
193
|
+
yield* sql`UPDATE effect_agent_subscription_store_state SET storage_version=${CURRENT_SUBSCRIPTION_STORE_VERSION} WHERE singleton=1`;
|
|
194
|
+
yield* checkLayout(yield* readObjects, CURRENT_SUBSCRIPTION_STORE_VERSION);
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
197
|
+
.pipe(Effect.catchTag("SqlError", () => unavailable("initialize subscription storage")));
|
|
160
198
|
});
|
|
161
199
|
|
|
162
200
|
const makeSubscriptionStore = Effect.fnUntraced(function* (owned: SourcePartition) {
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
canonicalRecordJson,
|
|
16
16
|
makeSqlThreadArchiveRange,
|
|
17
17
|
} from "@yielded/agent-storage-sql/sql-thread-archive-range";
|
|
18
|
-
import { makeProgressAppendValidation } from "@yielded/agent-storage-sql/sql-thread-native-reads";
|
|
19
18
|
import { makeSqlThreadWork } from "@yielded/agent-storage-sql/sql-thread-work";
|
|
20
19
|
import { ThreadId } from "@yielded/agent/identifiers";
|
|
21
20
|
import {
|
|
@@ -395,10 +394,6 @@ const makeJournal = (
|
|
|
395
394
|
cause,
|
|
396
395
|
});
|
|
397
396
|
|
|
398
|
-
const validateProgress = yield* makeProgressAppendValidation().pipe(
|
|
399
|
-
Effect.provideService(SqlClient.SqlClient, sql),
|
|
400
|
-
);
|
|
401
|
-
|
|
402
397
|
const executions = yield* makeSqlExecutions().pipe(
|
|
403
398
|
Effect.provideService(SqlClient.SqlClient, sql),
|
|
404
399
|
);
|
|
@@ -739,7 +734,6 @@ const makeJournal = (
|
|
|
739
734
|
);
|
|
740
735
|
|
|
741
736
|
yield* executionConflict(executions.validate(request));
|
|
742
|
-
yield* validateProgress(request);
|
|
743
737
|
|
|
744
738
|
const result = RawAppendResult.make({
|
|
745
739
|
firstSequence,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { backfillPeerMessageRetention } from "@yielded/agent-storage-sql/sql-message-delivery-store";
|
|
2
|
+
import {
|
|
3
|
+
SQL_PROMPT_PREDICATE,
|
|
4
|
+
upgradeExecutionCheckpoints,
|
|
5
|
+
} from "@yielded/agent-storage-sql/sql-thread-native-reads";
|
|
2
6
|
import { createSqlThreadWorkTables } from "@yielded/agent-storage-sql/sql-thread-work";
|
|
3
7
|
import { makeSqliteLayoutInspection } from "@yielded/agent-storage-sql/sqlite-layout-inspection";
|
|
4
8
|
import { CURRENT_RECORD_FORMAT } from "@yielded/agent/records";
|
|
@@ -12,7 +16,7 @@ import {
|
|
|
12
16
|
DoStorageError,
|
|
13
17
|
} from "../DoStorageError.ts";
|
|
14
18
|
|
|
15
|
-
export const CurrentDoStorageVersion =
|
|
19
|
+
export const CurrentDoStorageVersion = 27;
|
|
16
20
|
|
|
17
21
|
/** Fresh layout only. Unsupported stores are rejected before these statements execute. */
|
|
18
22
|
const layoutStatements = [
|
|
@@ -44,7 +48,7 @@ const layoutStatements = [
|
|
|
44
48
|
'CREATE INDEX effect_agent_records_subtree ON "effect_agent_canonical_records"(thread_id, source_submission_id, sequence) WHERE record_tag = 22',
|
|
45
49
|
'CREATE INDEX effect_agent_records_worker_input ON "effect_agent_canonical_records"(thread_id, message_id) WHERE record_tag = 34',
|
|
46
50
|
"CREATE INDEX effect_agent_submissions_nonterminal ON effect_agent_submissions (thread_id, queue_sequence) WHERE state <> 'settled'",
|
|
47
|
-
"CREATE TABLE effect_agent_message_deliveries ( owner_thread_id TEXT NOT NULL, message_id TEXT NOT NULL, version INTEGER NOT NULL, state TEXT NOT NULL, deadline_at_millis INTEGER, record_json TEXT NOT NULL, PRIMARY KEY (owner_thread_id, message_id) )",
|
|
51
|
+
"CREATE TABLE effect_agent_message_deliveries ( owner_thread_id TEXT NOT NULL, message_id TEXT NOT NULL, version INTEGER NOT NULL, state TEXT NOT NULL, deadline_at_millis INTEGER, record_json TEXT NOT NULL, peer_retained_until_millis INTEGER, PRIMARY KEY (owner_thread_id, message_id) )",
|
|
48
52
|
"CREATE INDEX effect_agent_message_deliveries_due ON effect_agent_message_deliveries (deadline_at_millis, owner_thread_id, message_id) WHERE deadline_at_millis IS NOT NULL",
|
|
49
53
|
"CREATE INDEX effect_agent_message_deliveries_pending ON \"effect_agent_message_deliveries\"(owner_thread_id, message_id) WHERE state NOT IN ('processed', 'refused')",
|
|
50
54
|
"CREATE TABLE effect_agent_worker_stops (thread_id TEXT PRIMARY KEY NOT NULL, terminal TEXT)",
|
|
@@ -60,7 +64,8 @@ const layoutStatements = [
|
|
|
60
64
|
"CREATE INDEX effect_agent_records_worker_completed ON effect_agent_canonical_records(thread_id, message_id, sequence) WHERE record_tag = 32",
|
|
61
65
|
"CREATE INDEX effect_agent_records_worker_stop ON effect_agent_canonical_records(thread_id, worker_thread_id, sequence) WHERE record_tag = 38",
|
|
62
66
|
"CREATE INDEX effect_agent_message_deliveries_identity ON effect_agent_message_deliveries(owner_thread_id, json_quote(message_id))",
|
|
63
|
-
"CREATE INDEX
|
|
67
|
+
"CREATE INDEX effect_agent_message_deliveries_peer_retained ON effect_agent_message_deliveries(owner_thread_id, peer_retained_until_millis, message_id) WHERE peer_retained_until_millis IS NOT NULL",
|
|
68
|
+
"CREATE INDEX effect_agent_message_deliveries_peer ON effect_agent_message_deliveries(owner_thread_id, message_id) WHERE state NOT IN ('processed', 'refused') AND json_extract(record_json, '$.envelope.messageAdmission.peerName') IS NOT NULL",
|
|
64
69
|
"CREATE TABLE effect_agent_journal_ranges (thread_id TEXT NOT NULL, first_sequence INTEGER NOT NULL, last_sequence INTEGER NOT NULL, record_count INTEGER NOT NULL, batch_count INTEGER NOT NULL, byte_count INTEGER NOT NULL, state TEXT NOT NULL, locator TEXT, PRIMARY KEY (thread_id, first_sequence), FOREIGN KEY (thread_id) REFERENCES effect_agent_threads(thread_id) ON DELETE RESTRICT)",
|
|
65
70
|
"CREATE TABLE effect_agent_archive_batches (thread_id TEXT NOT NULL, batch_id TEXT NOT NULL, range_first_sequence INTEGER NOT NULL, batch_json TEXT NOT NULL, PRIMARY KEY (thread_id, batch_id), FOREIGN KEY (thread_id, batch_id) REFERENCES effect_agent_canonical_batches(thread_id, batch_id) ON DELETE RESTRICT, CONSTRAINT effect_agent_archive_batches_range_fkey FOREIGN KEY (thread_id, range_first_sequence) REFERENCES effect_agent_journal_ranges(thread_id, first_sequence) ON DELETE RESTRICT)",
|
|
66
71
|
"CREATE TABLE effect_agent_archive_records (thread_id TEXT NOT NULL, sequence INTEGER NOT NULL, range_first_sequence INTEGER NOT NULL, record_json TEXT NOT NULL, PRIMARY KEY (thread_id, sequence), FOREIGN KEY (thread_id, sequence) REFERENCES effect_agent_canonical_records(thread_id, sequence) ON DELETE RESTRICT, CONSTRAINT effect_agent_archive_records_range_fkey FOREIGN KEY (thread_id, range_first_sequence) REFERENCES effect_agent_journal_ranges(thread_id, first_sequence) ON DELETE RESTRICT)",
|
|
@@ -82,13 +87,14 @@ const layoutStatements = [
|
|
|
82
87
|
const headerStatement =
|
|
83
88
|
"CREATE TABLE effect_agent_schema (singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1), layout_version INTEGER NOT NULL CHECK (layout_version > 0), record_format TEXT NOT NULL CHECK (length(record_format) > 0))";
|
|
84
89
|
|
|
85
|
-
const layoutObjects =
|
|
86
|
-
|
|
90
|
+
const layoutObjects = (statements: ReadonlyArray<string>) =>
|
|
91
|
+
statements.map((statement, index) => {
|
|
92
|
+
const name = /^CREATE (TABLE|(?:UNIQUE )?INDEX|TRIGGER) "?([a-z_][a-z_0-9]*)"?/.exec(statement);
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
if (name === null) throw new Error("Invalid fresh layout statement");
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
});
|
|
96
|
+
return [name[1].replace("UNIQUE ", "").toLowerCase(), name[2], index] as const;
|
|
97
|
+
});
|
|
92
98
|
|
|
93
99
|
const Legacy = Schema.Tuple([
|
|
94
100
|
Schema.Struct({
|
|
@@ -115,22 +121,43 @@ const incompatible = (actualVersion: number, message: string) =>
|
|
|
115
121
|
const storageError = (cause: SqlError) =>
|
|
116
122
|
DoStorageError.make({ operation: "inspect storage layout", cause, message: cause.message });
|
|
117
123
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})
|
|
124
|
+
const inspection = (version: number, statements: ReadonlyArray<string>) =>
|
|
125
|
+
makeSqliteLayoutInspection({
|
|
126
|
+
version,
|
|
127
|
+
statements,
|
|
128
|
+
objects: layoutObjects(statements),
|
|
129
|
+
headerStatement,
|
|
130
|
+
incompatible,
|
|
131
|
+
storageError,
|
|
132
|
+
corruption: (table) =>
|
|
133
|
+
DoStorageCorruptionError.make({
|
|
134
|
+
table,
|
|
135
|
+
rowKey: "schema",
|
|
136
|
+
message: "Malformed storage layout or version header.",
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
132
139
|
|
|
133
|
-
|
|
140
|
+
// Released predecessors retain the same record format and canonical tables.
|
|
141
|
+
const addedIndexes = new Set(["effect_agent_message_deliveries_peer_retained"]);
|
|
142
|
+
|
|
143
|
+
const layout25Statements = layoutObjects(layoutStatements)
|
|
144
|
+
.filter(([, name]) => !addedIndexes.has(name))
|
|
145
|
+
.map(([, , index]) =>
|
|
146
|
+
layoutStatements[index]
|
|
147
|
+
.replace(", peer_retained_until_millis INTEGER", "")
|
|
148
|
+
.replace(
|
|
149
|
+
"'$.envelope.messageAdmission.peerName') IS NOT NULL",
|
|
150
|
+
"'$.envelope.messageAdmission.schemaVersion') = 1",
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const { decode, readObjects, readHeader } = inspection(CurrentDoStorageVersion, layoutStatements);
|
|
155
|
+
const layout25 = inspection(25, layout25Statements);
|
|
156
|
+
const layout26 = inspection(26, layoutStatements);
|
|
157
|
+
|
|
158
|
+
const inspectStorage = Effect.fnUntraced(function* (
|
|
159
|
+
allowUpgrade = false,
|
|
160
|
+
): Effect.fn.Return<
|
|
134
161
|
DoStorageHeader | undefined,
|
|
135
162
|
DoStorageCompatibilityError | DoStorageCorruptionError | DoStorageError,
|
|
136
163
|
SqlClient.SqlClient
|
|
@@ -157,6 +184,17 @@ const inspectStorage = Effect.fnUntraced(function* (): Effect.fn.Return<
|
|
|
157
184
|
|
|
158
185
|
const version = legacy.value;
|
|
159
186
|
|
|
187
|
+
if (allowUpgrade && (version === 25 || version === 26)) {
|
|
188
|
+
if (version === 25 && objects.some(({ name }) => addedIndexes.has(name)))
|
|
189
|
+
return yield* incompatible(version, "Mixed layout 25 and 26 objects.");
|
|
190
|
+
const predecessor = version === 25 ? layout25 : layout26;
|
|
191
|
+
const header = yield* predecessor.readHeader(objects, version);
|
|
192
|
+
|
|
193
|
+
yield* predecessor.readManagedTriggers(true);
|
|
194
|
+
|
|
195
|
+
return header;
|
|
196
|
+
}
|
|
197
|
+
|
|
160
198
|
return yield* readHeader(objects, version);
|
|
161
199
|
});
|
|
162
200
|
|
|
@@ -170,15 +208,46 @@ export const readDoStorageHeader = Effect.fnUntraced(function* () {
|
|
|
170
208
|
return header;
|
|
171
209
|
});
|
|
172
210
|
|
|
173
|
-
/**
|
|
211
|
+
/** Initialize fresh storage or atomically upgrade the exact released layouts 25 and 26. */
|
|
174
212
|
export const ensureDoStorageLayout = Effect.fn("DoStorage.initializeLayout")(function* () {
|
|
175
213
|
const sql = (yield* SqlClient.SqlClient).withoutTransforms();
|
|
176
214
|
|
|
177
215
|
return yield* sql
|
|
178
216
|
.withTransaction(
|
|
179
217
|
Effect.gen(function* () {
|
|
180
|
-
const header = yield* inspectStorage();
|
|
218
|
+
const header = yield* inspectStorage(true);
|
|
219
|
+
|
|
220
|
+
if (header?.layoutVersion === 25) {
|
|
221
|
+
yield* sql`ALTER TABLE effect_agent_message_deliveries ADD COLUMN peer_retained_until_millis INTEGER`;
|
|
222
|
+
yield* backfillPeerMessageRetention().pipe(
|
|
223
|
+
Effect.catchTag("MessageDeliveryError", () =>
|
|
224
|
+
DoStorageCorruptionError.make({
|
|
225
|
+
table: "effect_agent_message_deliveries",
|
|
226
|
+
rowKey: "upgrade",
|
|
227
|
+
message: "Invalid layout-25 delivery evidence; upgrade rolled back.",
|
|
228
|
+
}),
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
yield* sql`DROP INDEX effect_agent_message_deliveries_peer`;
|
|
232
|
+
for (const [, name, index] of layoutObjects(layoutStatements))
|
|
233
|
+
if (addedIndexes.has(name) || name === "effect_agent_message_deliveries_peer")
|
|
234
|
+
yield* sql.unsafe(layoutStatements[index]).withoutTransform;
|
|
235
|
+
}
|
|
236
|
+
if (header?.layoutVersion === 25 || header?.layoutVersion === 26) {
|
|
237
|
+
yield* upgradeExecutionCheckpoints(undefined).pipe(
|
|
238
|
+
Effect.catchTag("ThreadStoreError", (cause) =>
|
|
239
|
+
DoStorageCorruptionError.make({
|
|
240
|
+
table: "effect_agent_executions",
|
|
241
|
+
rowKey: "upgrade",
|
|
242
|
+
message: `Invalid retained execution; upgrade rolled back: ${cause.message}`,
|
|
243
|
+
}),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
yield* sql`UPDATE effect_agent_schema SET layout_version=${CurrentDoStorageVersion} WHERE singleton=1`;
|
|
247
|
+
yield* sql`UPDATE effect_agent_meta SET value=${String(CurrentDoStorageVersion)} WHERE key='storage_version'`;
|
|
181
248
|
|
|
249
|
+
return yield* readDoStorageHeader();
|
|
250
|
+
}
|
|
182
251
|
if (header !== undefined) return header;
|
|
183
252
|
for (const statement of layoutStatements) yield* sql.unsafe(statement).withoutTransform;
|
|
184
253
|
yield* createSqlThreadWorkTables().pipe(
|