@hraness/oh 0.4.0 → 0.4.2
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/LICENSE +3 -0
- package/README.md +12 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15590 -1705
- package/dist/index.js +15209 -261
- package/dist/libsql-model.d.ts +75 -0
- package/dist/libsql-model.d.ts.map +1 -0
- package/dist/libsql-platform.d.ts +24 -0
- package/dist/libsql-platform.d.ts.map +1 -0
- package/dist/libsql-program.d.ts +64 -0
- package/dist/libsql-program.d.ts.map +1 -0
- package/dist/libsql-runtime.d.ts +13 -0
- package/dist/libsql-runtime.d.ts.map +1 -0
- package/dist/libsql.d.ts +4 -36
- package/dist/libsql.d.ts.map +1 -1
- package/dist/libsql.js +16583 -1426
- package/dist/memory-authority-platform.d.ts +210 -0
- package/dist/memory-authority-platform.d.ts.map +1 -0
- package/dist/memory-authority-program.d.ts +15 -0
- package/dist/memory-authority-program.d.ts.map +1 -0
- package/dist/memory-authority-runtime.d.ts +4 -0
- package/dist/memory-authority-runtime.d.ts.map +1 -0
- package/dist/memory-core.d.ts +522 -0
- package/dist/memory-core.d.ts.map +1 -0
- package/dist/memory.d.ts +5 -445
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +18232 -4143
- package/dist/sdk.js +15266 -318
- package/dist/semantic-cloud.js +1 -260
- package/dist/semantic-model.d.ts +75 -0
- package/dist/semantic-model.d.ts.map +1 -0
- package/dist/semantic-platform.d.ts +32 -0
- package/dist/semantic-platform.d.ts.map +1 -0
- package/dist/semantic-program.d.ts +18 -0
- package/dist/semantic-program.d.ts.map +1 -0
- package/dist/semantic-runtime.d.ts +6 -0
- package/dist/semantic-runtime.d.ts.map +1 -0
- package/dist/semantic.d.ts +6 -57
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +15541 -514
- package/dist/sqlite/index.js +1148 -1378
- package/dist/sqlite/port.d.ts +1 -1
- package/dist/sqlite/port.d.ts.map +1 -1
- package/dist/sync-libsql-program.d.ts +5 -0
- package/dist/sync-libsql-program.d.ts.map +1 -0
- package/dist/sync-model.d.ts +60 -0
- package/dist/sync-model.d.ts.map +1 -0
- package/dist/sync-platform.d.ts +41 -0
- package/dist/sync-platform.d.ts.map +1 -0
- package/dist/sync-program.d.ts +11 -0
- package/dist/sync-program.d.ts.map +1 -0
- package/dist/sync-runtime.d.ts +6 -0
- package/dist/sync-runtime.d.ts.map +1 -0
- package/dist/sync.d.ts +5 -55
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +15174 -226
- package/package.json +7 -5
- package/skills/oh/SKILL.md +3 -2
- package/spec/v1/store.md +14 -0
- package/src/cli.test.ts +35 -0
- package/src/cli.ts +4 -3
- package/src/cloudflare-embedding.ts +1 -1
- package/src/libsql-lifecycle.test.ts +76 -0
- package/src/libsql-model.ts +350 -0
- package/src/libsql-platform.ts +51 -0
- package/src/libsql-program.ts +1583 -0
- package/src/libsql-runtime.ts +89 -0
- package/src/libsql-semantic-v2.ts +1 -1
- package/src/libsql-semantic.ts +1 -1
- package/src/libsql.test.ts +245 -0
- package/src/libsql.ts +26 -1698
- package/src/memory-authority-platform.ts +133 -0
- package/src/memory-authority-program.ts +309 -0
- package/src/memory-authority-runtime.ts +40 -0
- package/src/memory-core.ts +2401 -0
- package/src/memory.test.ts +55 -0
- package/src/memory.ts +68 -2759
- package/src/semantic-model.ts +142 -0
- package/src/semantic-platform.ts +130 -0
- package/src/semantic-program.ts +113 -0
- package/src/semantic-runtime.ts +53 -0
- package/src/semantic.test.ts +115 -1
- package/src/semantic.ts +14 -324
- package/src/sqlite/port.ts +1 -1
- package/src/sync-libsql-program.ts +170 -0
- package/src/sync-lifecycle.test.ts +110 -0
- package/src/sync-model.ts +530 -0
- package/src/sync-platform.ts +87 -0
- package/src/sync-program.ts +117 -0
- package/src/sync-runtime.ts +33 -0
- package/src/sync.ts +10 -765
|
@@ -0,0 +1,1583 @@
|
|
|
1
|
+
import { Deferred, Effect, Either, Ref } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
canonicalJson,
|
|
4
|
+
canonicalSha256,
|
|
5
|
+
parseSha256Hex,
|
|
6
|
+
safeCode,
|
|
7
|
+
utf8ByteLength,
|
|
8
|
+
type Sha256Hex,
|
|
9
|
+
} from "./canonical";
|
|
10
|
+
import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
|
|
11
|
+
import {
|
|
12
|
+
canonicalKnowledgeGraphChangesV1,
|
|
13
|
+
knowledgeGraphRecordRefV1,
|
|
14
|
+
OH_GRAPH_LIMITS_V1,
|
|
15
|
+
parseKnowledgeGraphRecordV1,
|
|
16
|
+
type KnowledgeGraphRecordV1,
|
|
17
|
+
} from "./graph";
|
|
18
|
+
import { OH_OPERATION_MAX_BYTES_V1, type OhOperationV1 } from "./operation";
|
|
19
|
+
import {
|
|
20
|
+
createOhDependencyClosureV1,
|
|
21
|
+
createOhSpacePurgeReceiptV1,
|
|
22
|
+
createOhStoreBindingV1,
|
|
23
|
+
emptyOhHeadV1,
|
|
24
|
+
OH_CANONICAL_STORE_PROFILE_V1,
|
|
25
|
+
OH_WORKING_STORE_PROFILE_V1,
|
|
26
|
+
OhConflictError,
|
|
27
|
+
OhIntegrityError,
|
|
28
|
+
OhOperationSizeError,
|
|
29
|
+
OhProfileError,
|
|
30
|
+
OhPurgedSpaceError,
|
|
31
|
+
parseOhHeadRefV1,
|
|
32
|
+
parseOhStoreProfileV1,
|
|
33
|
+
replayOhOperationsV1,
|
|
34
|
+
transitionOhSnapshotV1,
|
|
35
|
+
type OhChangesPageV1,
|
|
36
|
+
type OhCommitInputV1,
|
|
37
|
+
type OhDependencyClosureV1,
|
|
38
|
+
type OhHeadRefV1,
|
|
39
|
+
type OhHeadV1,
|
|
40
|
+
type OhSnapshotV1,
|
|
41
|
+
type OhSpacePurgeReceiptV1,
|
|
42
|
+
type OhStoreBindingV1,
|
|
43
|
+
type OhStoreVerificationV1,
|
|
44
|
+
} from "./store";
|
|
45
|
+
import {
|
|
46
|
+
type OhLibSqlStatementV1,
|
|
47
|
+
type OhLibSqlResultV1,
|
|
48
|
+
type OhLibSqlStoreAuthorityOptionsV1,
|
|
49
|
+
OH_LIBSQL_STORE_LIMITS_V1,
|
|
50
|
+
AUTHORITY_SCHEMA_NAME,
|
|
51
|
+
AUTHORITY_SCHEMA_VERSION,
|
|
52
|
+
EMPTY_RECORDS_SHA256,
|
|
53
|
+
PURGE_ROW_SELECT,
|
|
54
|
+
BINDING_ROW_SELECT,
|
|
55
|
+
SPACE_PURGE_PROOF_SELECT,
|
|
56
|
+
OPERATION_ROW_COLUMNS,
|
|
57
|
+
OPERATION_RESPONSE_BYTES,
|
|
58
|
+
RECORD_RESPONSE_BYTES,
|
|
59
|
+
DEPENDENCY_RESPONSE_BYTES,
|
|
60
|
+
OPERATION_RECORD_RESPONSE_BYTES,
|
|
61
|
+
AUTHORITY_SCHEMA_TABLE_STATEMENT,
|
|
62
|
+
AUTHORITY_SCHEMA_STATEMENTS,
|
|
63
|
+
normalizedSchemaSql,
|
|
64
|
+
AUTHORITY_SCHEMA_OBJECTS,
|
|
65
|
+
AUTHORITY_SCHEMA_SHA256,
|
|
66
|
+
rowValue,
|
|
67
|
+
integer,
|
|
68
|
+
normalizeLimit,
|
|
69
|
+
parseOperationRow,
|
|
70
|
+
parseBindingRow,
|
|
71
|
+
parsePurgeReceiptRow,
|
|
72
|
+
parseHeadRow,
|
|
73
|
+
PURGE_PAYLOAD_TABLES,
|
|
74
|
+
} from "./libsql-model";
|
|
75
|
+
import {
|
|
76
|
+
libSqlValue,
|
|
77
|
+
libSqlFailure,
|
|
78
|
+
type LibSqlFailure,
|
|
79
|
+
type LibSqlAuthorityClientService,
|
|
80
|
+
} from "./libsql-platform";
|
|
81
|
+
|
|
82
|
+
function queryOne(
|
|
83
|
+
client: LibSqlAuthorityClientService,
|
|
84
|
+
statement: OhLibSqlStatementV1,
|
|
85
|
+
): Effect.Effect<Readonly<Record<string, unknown>> | readonly unknown[] | null, LibSqlFailure> {
|
|
86
|
+
return Effect.gen(function* () {
|
|
87
|
+
return (yield* client.execute(statement)).rows[0] ?? null;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function verifyAuthoritySchemaObjects(client: LibSqlAuthorityClientService): Effect.Effect<void, LibSqlFailure> {
|
|
92
|
+
return Effect.gen(function* () {
|
|
93
|
+
const rows = (yield* client.execute({
|
|
94
|
+
sql: `SELECT type, name, tbl_name, sql FROM sqlite_schema
|
|
95
|
+
WHERE sql IS NOT NULL AND (name = 'oh_authority_schemas' OR name GLOB 'oh_authority_*'
|
|
96
|
+
OR tbl_name GLOB 'oh_authority_*')
|
|
97
|
+
ORDER BY type, name` })).rows;
|
|
98
|
+
const actual = (yield* libSqlValue(() => rows.map((row) => {
|
|
99
|
+
const type = rowValue(row, "type", 0);
|
|
100
|
+
const name = rowValue(row, "name", 1);
|
|
101
|
+
const tableName = rowValue(row, "tbl_name", 2);
|
|
102
|
+
const sql = rowValue(row, "sql", 3);
|
|
103
|
+
if ((type !== "table" && type !== "index" && type !== "trigger") || typeof name !== "string"
|
|
104
|
+
|| typeof tableName !== "string" || typeof sql !== "string") {
|
|
105
|
+
throw new OhIntegrityError("The installed libSQL authority has an invalid schema object.");
|
|
106
|
+
}
|
|
107
|
+
return { name, sql: normalizedSchemaSql(sql), tableName, type };
|
|
108
|
+
})));
|
|
109
|
+
if ((yield* libSqlValue(() => canonicalJson(actual))) !== (yield* libSqlValue(() => canonicalJson(AUTHORITY_SCHEMA_OBJECTS)))) {
|
|
110
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The installed libSQL authority objects differ from this runtime.")));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function verifyAuthoritySchema(client: LibSqlAuthorityClientService): Effect.Effect<void, LibSqlFailure> {
|
|
116
|
+
return Effect.gen(function* () {
|
|
117
|
+
const installed = yield* queryOne(client, {
|
|
118
|
+
sql: `SELECT name, schema_sha256
|
|
119
|
+
FROM oh_authority_schemas WHERE version = ?`, args: [AUTHORITY_SCHEMA_VERSION]
|
|
120
|
+
});
|
|
121
|
+
if (installed === null || rowValue(installed, "name", 0) !== AUTHORITY_SCHEMA_NAME
|
|
122
|
+
|| rowValue(installed, "schema_sha256", 1) !== AUTHORITY_SCHEMA_SHA256) {
|
|
123
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The installed libSQL authority schema differs from this runtime.")));
|
|
124
|
+
}
|
|
125
|
+
const contract = yield* queryOne(client, {
|
|
126
|
+
sql: `SELECT contract_sha256, manifest_json
|
|
127
|
+
FROM oh_authority_contracts WHERE contract_id = ?`, args: [OH_CONTRACT_MANIFEST_V1.contractId]
|
|
128
|
+
});
|
|
129
|
+
if (contract === null || rowValue(contract, "contract_sha256", 0) !== OH_CONTRACT_MANIFEST_V1.contractSha256
|
|
130
|
+
|| rowValue(contract, "manifest_json", 1) !== (yield* libSqlValue(() => canonicalJson(OH_CONTRACT_MANIFEST_V1)))) {
|
|
131
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority contract differs from this runtime.")));
|
|
132
|
+
}
|
|
133
|
+
yield* verifyAuthoritySchemaObjects(client);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One-time schema operation for a client authorized to create authority tables. */
|
|
138
|
+
export function bootstrapAuthority(
|
|
139
|
+
client: LibSqlAuthorityClientService,
|
|
140
|
+
): Effect.Effect<Readonly<{ schemaSha256: Sha256Hex; schemaVersion: 1; v: 1 }>, LibSqlFailure> {
|
|
141
|
+
return Effect.gen(function* () {
|
|
142
|
+
const existingObjects = (yield* client.execute({
|
|
143
|
+
sql: `SELECT name FROM sqlite_schema
|
|
144
|
+
WHERE sql IS NOT NULL AND (name = 'oh_authority_schemas' OR name GLOB 'oh_authority_*'
|
|
145
|
+
OR tbl_name GLOB 'oh_authority_*')` })).rows;
|
|
146
|
+
if (existingObjects.length > 0) {
|
|
147
|
+
if (!(yield* libSqlValue(() => existingObjects.some((row) => rowValue(row, "name", 0) === "oh_authority_schemas")))) {
|
|
148
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("Refusing to bootstrap over preexisting Oh authority objects.")));
|
|
149
|
+
}
|
|
150
|
+
yield* verifyAuthoritySchema(client);
|
|
151
|
+
return { schemaSha256: AUTHORITY_SCHEMA_SHA256, schemaVersion: 1, v: 1 };
|
|
152
|
+
}
|
|
153
|
+
const setup: OhLibSqlStatementV1[] = (yield* libSqlValue(() => [AUTHORITY_SCHEMA_TABLE_STATEMENT, ...AUTHORITY_SCHEMA_STATEMENTS]
|
|
154
|
+
.map((sql) => ({ sql }))));
|
|
155
|
+
setup.push({
|
|
156
|
+
sql: `INSERT INTO oh_authority_schemas(version, name, schema_sha256, applied_at)
|
|
157
|
+
VALUES (?, ?, ?, ?) ON CONFLICT(version) DO NOTHING`,
|
|
158
|
+
args: [AUTHORITY_SCHEMA_VERSION, AUTHORITY_SCHEMA_NAME, AUTHORITY_SCHEMA_SHA256, (yield* client.currentInstant)]
|
|
159
|
+
});
|
|
160
|
+
setup.push({
|
|
161
|
+
sql: `INSERT INTO oh_authority_contracts(contract_id, contract_sha256, manifest_json)
|
|
162
|
+
VALUES (?, ?, ?) ON CONFLICT(contract_id) DO NOTHING`, args: [OH_CONTRACT_MANIFEST_V1.contractId,
|
|
163
|
+
OH_CONTRACT_MANIFEST_V1.contractSha256, (yield* libSqlValue(() => canonicalJson(OH_CONTRACT_MANIFEST_V1)))]
|
|
164
|
+
});
|
|
165
|
+
yield* client.batch(setup, "write");
|
|
166
|
+
yield* verifyAuthoritySchema(client);
|
|
167
|
+
return { schemaSha256: AUTHORITY_SCHEMA_SHA256, schemaVersion: 1, v: 1 };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function initializeSpace(
|
|
172
|
+
client: LibSqlAuthorityClientService,
|
|
173
|
+
binding: OhStoreBindingV1,
|
|
174
|
+
): Effect.Effect<void, LibSqlFailure> {
|
|
175
|
+
return Effect.gen(function* () {
|
|
176
|
+
const purged = yield* queryOne(client, {
|
|
177
|
+
sql: PURGE_ROW_SELECT,
|
|
178
|
+
args: [binding.spaceId]
|
|
179
|
+
});
|
|
180
|
+
if (purged !== null) return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError((yield* libSqlValue(() => parsePurgeReceiptRow(
|
|
181
|
+
purged, binding.spaceId, binding.bindingSha256))))));
|
|
182
|
+
const now = yield* client.currentInstant;
|
|
183
|
+
const attempt = yield* Effect.either(client.batch([
|
|
184
|
+
{
|
|
185
|
+
sql: `INSERT INTO oh_authority_spaces(space_id, contract_id, generation,
|
|
186
|
+
head_operation_sha256, graph_revision_sha256, records_sha256, sequence, created_at, updated_at)
|
|
187
|
+
SELECT ?, ?, 0, NULL, NULL, ?, 0, ?, ?
|
|
188
|
+
WHERE NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?)
|
|
189
|
+
ON CONFLICT(space_id) DO NOTHING`,
|
|
190
|
+
args: [binding.spaceId, OH_CONTRACT_MANIFEST_V1.contractId, EMPTY_RECORDS_SHA256, now, now,
|
|
191
|
+
binding.spaceId]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
sql: `INSERT INTO oh_authority_bindings(space_id, realm_id, profile_id, profile_kind,
|
|
195
|
+
profile_sha256, binding_sha256, binding_json, created_at)
|
|
196
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?
|
|
197
|
+
WHERE NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?)
|
|
198
|
+
AND EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
199
|
+
ON CONFLICT(space_id) DO NOTHING`,
|
|
200
|
+
args: [binding.spaceId, binding.realmId, binding.profile.profileId, binding.profile.profileKind,
|
|
201
|
+
binding.profile.profileSha256, binding.bindingSha256, (yield* libSqlValue(() => canonicalJson(binding))), now,
|
|
202
|
+
binding.spaceId, binding.spaceId]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
sql: `INSERT INTO oh_authority_commit_guards(value)
|
|
206
|
+
SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?)
|
|
207
|
+
OR NOT EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
208
|
+
OR NOT EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ? AND binding_sha256 = ?)`,
|
|
209
|
+
args: [binding.spaceId, binding.spaceId, binding.spaceId, binding.bindingSha256]
|
|
210
|
+
},
|
|
211
|
+
], "write"));
|
|
212
|
+
if (Either.isLeft(attempt)) {
|
|
213
|
+
const error = attempt.left;
|
|
214
|
+
const raced = yield* queryOne(client, {
|
|
215
|
+
sql: PURGE_ROW_SELECT,
|
|
216
|
+
args: [binding.spaceId]
|
|
217
|
+
});
|
|
218
|
+
if (raced !== null) return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError((yield* libSqlValue(() => parsePurgeReceiptRow(
|
|
219
|
+
raced, binding.spaceId, binding.bindingSha256))))));
|
|
220
|
+
const persisted = yield* queryOne(client, { sql: BINDING_ROW_SELECT, args: [binding.spaceId] });
|
|
221
|
+
if (persisted !== null
|
|
222
|
+
&& (yield* libSqlValue(() => canonicalJson(parseBindingRow(persisted, binding.spaceId)))) !== (yield* libSqlValue(() => canonicalJson(binding)))) {
|
|
223
|
+
return yield* Effect.fail(libSqlFailure(new OhProfileError("The remote space is already bound to a different realm or profile.")));
|
|
224
|
+
}
|
|
225
|
+
return yield* Effect.fail(error);
|
|
226
|
+
}
|
|
227
|
+
const persisted = yield* queryOne(client, { sql: BINDING_ROW_SELECT, args: [binding.spaceId] });
|
|
228
|
+
if (persisted === null) {
|
|
229
|
+
const raced = yield* queryOne(client, {
|
|
230
|
+
sql: PURGE_ROW_SELECT,
|
|
231
|
+
args: [binding.spaceId]
|
|
232
|
+
});
|
|
233
|
+
if (raced !== null) return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError((yield* libSqlValue(() => parsePurgeReceiptRow(
|
|
234
|
+
raced, binding.spaceId, binding.bindingSha256))))));
|
|
235
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote space has no persisted binding after initialization.")));
|
|
236
|
+
}
|
|
237
|
+
if ((yield* libSqlValue(() => canonicalJson(parseBindingRow(persisted, binding.spaceId)))) !== (yield* libSqlValue(() => canonicalJson(binding)))) {
|
|
238
|
+
return yield* Effect.fail(libSqlFailure(new OhProfileError("The remote space is already bound to a different realm or profile.")));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function requireExistingSpace(
|
|
244
|
+
client: LibSqlAuthorityClientService,
|
|
245
|
+
binding: OhStoreBindingV1,
|
|
246
|
+
): Effect.Effect<void, LibSqlFailure> {
|
|
247
|
+
return Effect.gen(function* () {
|
|
248
|
+
const results = yield* client.batch([
|
|
249
|
+
{ sql: BINDING_ROW_SELECT, args: [binding.spaceId] },
|
|
250
|
+
{
|
|
251
|
+
sql: `SELECT generation, graph_revision_sha256, head_operation_sha256,
|
|
252
|
+
records_sha256, sequence, contract_id FROM oh_authority_spaces WHERE space_id = ?`,
|
|
253
|
+
args: [binding.spaceId]
|
|
254
|
+
},
|
|
255
|
+
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] },
|
|
256
|
+
], "read");
|
|
257
|
+
if (results.length !== 3) {
|
|
258
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete existing-space proof.")));
|
|
259
|
+
}
|
|
260
|
+
const [bindingResult, spaceResult, purgeResult] = results as
|
|
261
|
+
[OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1];
|
|
262
|
+
const purgeRow = purgeResult.rows[0];
|
|
263
|
+
if (purgeRow !== undefined) {
|
|
264
|
+
return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError((yield* libSqlValue(() => parsePurgeReceiptRow(
|
|
265
|
+
purgeRow, binding.spaceId, binding.bindingSha256))))));
|
|
266
|
+
}
|
|
267
|
+
const bindingRow = bindingResult.rows[0];
|
|
268
|
+
const spaceRow = spaceResult.rows[0];
|
|
269
|
+
if (bindingRow === undefined || spaceRow === undefined) {
|
|
270
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The requested remote Oh space does not already exist.")));
|
|
271
|
+
}
|
|
272
|
+
const persisted = (yield* libSqlValue(() => parseBindingRow(bindingRow, binding.spaceId)));
|
|
273
|
+
if ((yield* libSqlValue(() => canonicalJson(persisted))) !== (yield* libSqlValue(() => canonicalJson(binding)))) {
|
|
274
|
+
return yield* Effect.fail(libSqlFailure(new OhProfileError("The remote space is bound to a different realm or profile.")));
|
|
275
|
+
}
|
|
276
|
+
if (rowValue(spaceRow, "contract_id", 5) !== OH_CONTRACT_MANIFEST_V1.contractId) {
|
|
277
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The existing remote space uses a different Oh contract.")));
|
|
278
|
+
}
|
|
279
|
+
(yield* libSqlValue(() => parseHeadRow(spaceRow)));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function assertRemotePurgeComplete(
|
|
284
|
+
client: LibSqlAuthorityClientService,
|
|
285
|
+
binding: OhStoreBindingV1,
|
|
286
|
+
expected: OhSpacePurgeReceiptV1,
|
|
287
|
+
): Effect.Effect<void, LibSqlFailure> {
|
|
288
|
+
return Effect.gen(function* () {
|
|
289
|
+
const results = yield* client.batch([
|
|
290
|
+
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] },
|
|
291
|
+
...(yield* libSqlValue(() => PURGE_PAYLOAD_TABLES.map((table) => ({
|
|
292
|
+
sql: `SELECT count(*) AS count FROM ${table} WHERE space_id = ?`,
|
|
293
|
+
args: [binding.spaceId],
|
|
294
|
+
})))),
|
|
295
|
+
{
|
|
296
|
+
sql: `SELECT count(*) AS count FROM oh_authority_operation_records AS materialized
|
|
297
|
+
LEFT JOIN oh_authority_operations AS operation
|
|
298
|
+
ON operation.operation_sha256 = materialized.operation_sha256
|
|
299
|
+
WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id` },
|
|
300
|
+
], "read");
|
|
301
|
+
const receiptRow = results[0]?.rows[0];
|
|
302
|
+
if (receiptRow === undefined
|
|
303
|
+
|| (yield* libSqlValue(() => canonicalJson(parsePurgeReceiptRow(receiptRow, binding.spaceId,
|
|
304
|
+
binding.bindingSha256)))) !== (yield* libSqlValue(() => canonicalJson(expected)))) {
|
|
305
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote purge receipt differs from the requested purge.")));
|
|
306
|
+
}
|
|
307
|
+
for (let index = 0; index < PURGE_PAYLOAD_TABLES.length; index += 1) {
|
|
308
|
+
const countRow = results[index + 1]?.rows[0];
|
|
309
|
+
if (countRow === undefined || integer(rowValue(countRow, "count", 0)) !== 0) {
|
|
310
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError(`Remote purge left rows in ${PURGE_PAYLOAD_TABLES[index]}.`)));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const orphanRow = results[PURGE_PAYLOAD_TABLES.length + 1]?.rows[0];
|
|
314
|
+
if (orphanRow === undefined || integer(rowValue(orphanRow, "count", 0)) !== 0) {
|
|
315
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("Remote purge left an orphaned or cross-space operation record.")));
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export class LibSqlStoreProgram {
|
|
321
|
+
readonly binding: OhStoreBindingV1;
|
|
322
|
+
readonly #client: LibSqlAuthorityClientService;
|
|
323
|
+
#purged: OhSpacePurgeReceiptV1 | null = null;
|
|
324
|
+
#hostPurge: OhSpacePurgeReceiptV1 | null = null;
|
|
325
|
+
|
|
326
|
+
constructor(client: LibSqlAuthorityClientService, binding: OhStoreBindingV1) {
|
|
327
|
+
this.#client = client;
|
|
328
|
+
this.binding = binding;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
closedFailure(): LibSqlFailure {
|
|
332
|
+
return this.#purged === null
|
|
333
|
+
? { _tag: "LibSqlClosed", cause: new Error("The Oh libSQL store is closed.") }
|
|
334
|
+
: libSqlFailure(new OhPurgedSpaceError(this.#purged));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
purgeFromHost(input: Readonly<{ purgedAt?: string }>): Effect.Effect<OhSpacePurgeReceiptV1, LibSqlFailure> {
|
|
338
|
+
return Effect.gen(this, function* () {
|
|
339
|
+
if (this.binding.profile.profileKind !== "working" || !this.binding.profile.capabilities.wholeSpacePurge) {
|
|
340
|
+
return yield* Effect.fail(libSqlFailure(new OhProfileError("This host handle is not bound to a purgeable working profile.")));
|
|
341
|
+
}
|
|
342
|
+
if (this.#hostPurge !== null) return this.#hostPurge;
|
|
343
|
+
this.#hostPurge = yield* this.purgeWorkingSpace(input.purgedAt ?? (yield* this.#client.currentInstant));
|
|
344
|
+
return this.#hostPurge;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#assertOpen(): void {
|
|
349
|
+
if (this.#purged !== null) throw new OhPurgedSpaceError(this.#purged);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
head(): Effect.Effect<OhHeadV1, LibSqlFailure> {
|
|
353
|
+
return Effect.gen(this, function* () {
|
|
354
|
+
yield* libSqlValue(() => this.#assertOpen());
|
|
355
|
+
const row = yield* queryOne(this.#client, {
|
|
356
|
+
sql: `SELECT generation, graph_revision_sha256,
|
|
357
|
+
head_operation_sha256, records_sha256, sequence FROM oh_authority_spaces WHERE space_id = ?`,
|
|
358
|
+
args: [this.binding.spaceId]
|
|
359
|
+
});
|
|
360
|
+
if (row === null) {
|
|
361
|
+
const purged = yield* this.#readPurge();
|
|
362
|
+
if (purged !== null) { this.#purged = purged; return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError(purged))); }
|
|
363
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote Oh space does not exist.")));
|
|
364
|
+
}
|
|
365
|
+
return (yield* libSqlValue(() => parseHeadRow(row)));
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#readPurge(): Effect.Effect<OhSpacePurgeReceiptV1 | null, LibSqlFailure> {
|
|
370
|
+
return Effect.gen(this, function* () {
|
|
371
|
+
const row = yield* queryOne(this.#client, {
|
|
372
|
+
sql: PURGE_ROW_SELECT,
|
|
373
|
+
args: [this.binding.spaceId]
|
|
374
|
+
});
|
|
375
|
+
if (row === null) return null;
|
|
376
|
+
return (yield* libSqlValue(() => parsePurgeReceiptRow(row, this.binding.spaceId, this.binding.bindingSha256)));
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#headAt(reference: OhHeadRefV1): Effect.Effect<OhHeadV1, LibSqlFailure> {
|
|
381
|
+
return Effect.gen(this, function* () {
|
|
382
|
+
const parsed = (yield* libSqlValue(() => parseOhHeadRefV1(reference)));
|
|
383
|
+
if (parsed === null) return yield* Effect.fail(libSqlFailure(new TypeError("Invalid Oh head reference.")));
|
|
384
|
+
if (parsed.sequence === 0) return emptyOhHeadV1();
|
|
385
|
+
const row = yield* queryOne(this.#client, {
|
|
386
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
387
|
+
WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, parsed.sequence]
|
|
388
|
+
});
|
|
389
|
+
if (row === null) return yield* Effect.fail(libSqlFailure(new OhConflictError("The requested head is not present in this space.")));
|
|
390
|
+
const operation = (yield* libSqlValue(() => parseOperationRow(row, { spaceId: this.binding.spaceId })));
|
|
391
|
+
if (operation.spaceId !== this.binding.spaceId || operation.sequence !== parsed.sequence
|
|
392
|
+
|| operation.operationSha256 !== parsed.operationSha256) {
|
|
393
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The requested sequence identifies a different operation head.")));
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
generation: operation.sequence, graphRevisionSha256: operation.graphRevisionSha256,
|
|
397
|
+
operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256,
|
|
398
|
+
sequence: operation.sequence, v: 1
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#currentMaterializedSnapshot(expectedHead: OhHeadV1, maximumRecords: number): Effect.Effect<OhSnapshotV1, LibSqlFailure> {
|
|
404
|
+
return Effect.gen(this, function* () {
|
|
405
|
+
const provenancePredicate = `operation.space_id = ? AND (operation.operation_sha256 IS ?
|
|
406
|
+
OR operation.operation_sha256 IN (SELECT record.operation_sha256
|
|
407
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?))`;
|
|
408
|
+
const results = yield* this.#client.batch([
|
|
409
|
+
{
|
|
410
|
+
sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
411
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId]
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
sql: `SELECT
|
|
415
|
+
(SELECT count(*) FROM (SELECT DISTINCT operation.operation_sha256
|
|
416
|
+
FROM oh_authority_operations AS operation WHERE ${provenancePredicate})) AS provenance_count,
|
|
417
|
+
(SELECT coalesce(sum(bytes), 0) FROM (SELECT DISTINCT operation.operation_sha256,
|
|
418
|
+
${OPERATION_RESPONSE_BYTES} AS bytes FROM oh_authority_operations AS operation
|
|
419
|
+
WHERE ${provenancePredicate})) AS provenance_bytes,
|
|
420
|
+
(SELECT count(*) FROM oh_authority_records WHERE space_id = ?) AS record_count,
|
|
421
|
+
(SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) FROM oh_authority_records AS record
|
|
422
|
+
WHERE record.space_id = ?) AS record_bytes,
|
|
423
|
+
(SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0)
|
|
424
|
+
FROM oh_authority_dependencies AS dependency
|
|
425
|
+
WHERE dependency.space_id = ?) AS dependency_bytes`,
|
|
426
|
+
args: [this.binding.spaceId, expectedHead.operationSha256, this.binding.spaceId,
|
|
427
|
+
this.binding.spaceId, expectedHead.operationSha256, this.binding.spaceId,
|
|
428
|
+
this.binding.spaceId, this.binding.spaceId, this.binding.spaceId]
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
sql: `SELECT record_key, kind, record_sha256, record_json, operation_sha256, sequence
|
|
432
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?
|
|
433
|
+
AND (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0)
|
|
434
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?) <= ? ORDER BY record_key`, args: [this.binding.spaceId,
|
|
435
|
+
this.binding.spaceId, OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes]
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
sql: `SELECT record_key, dependency_key FROM oh_authority_dependencies
|
|
439
|
+
AS dependency WHERE dependency.space_id = ?
|
|
440
|
+
AND (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0)
|
|
441
|
+
FROM oh_authority_dependencies AS dependency WHERE dependency.space_id = ?) <= ?
|
|
442
|
+
ORDER BY record_key, dependency_key`,
|
|
443
|
+
args: [this.binding.spaceId, this.binding.spaceId, OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes]
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS}
|
|
447
|
+
FROM oh_authority_operations AS operation
|
|
448
|
+
WHERE ${provenancePredicate}
|
|
449
|
+
AND (SELECT coalesce(sum(bytes), 0) FROM (SELECT DISTINCT candidate.operation_sha256,
|
|
450
|
+
${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS bytes
|
|
451
|
+
FROM oh_authority_operations AS candidate
|
|
452
|
+
WHERE candidate.space_id = ? AND (candidate.operation_sha256 IS ?
|
|
453
|
+
OR candidate.operation_sha256 IN (SELECT record.operation_sha256
|
|
454
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?)))) <= ?
|
|
455
|
+
ORDER BY operation.sequence`,
|
|
456
|
+
args: [this.binding.spaceId, expectedHead.operationSha256, this.binding.spaceId,
|
|
457
|
+
this.binding.spaceId, expectedHead.operationSha256, this.binding.spaceId,
|
|
458
|
+
OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes]
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
sql: `SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum
|
|
462
|
+
FROM oh_authority_operations WHERE space_id = ?`, args: [this.binding.spaceId]
|
|
463
|
+
},
|
|
464
|
+
], "read");
|
|
465
|
+
if (results.length !== 6) return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete snapshot batch.")));
|
|
466
|
+
const [headResult, sizeResult, recordResult, dependencyResult, provenanceResult, historyResult] = results as
|
|
467
|
+
[OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1,
|
|
468
|
+
OhLibSqlResultV1, OhLibSqlResultV1];
|
|
469
|
+
const headRow = headResult.rows[0];
|
|
470
|
+
if (headRow === undefined) {
|
|
471
|
+
const purge = yield* this.#readPurge();
|
|
472
|
+
if (purge !== null) { this.#purged = purge; return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError(purge))); }
|
|
473
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote Oh space disappeared while reading its snapshot.")));
|
|
474
|
+
}
|
|
475
|
+
const head = (yield* libSqlValue(() => parseHeadRow(headRow)));
|
|
476
|
+
if ((yield* libSqlValue(() => canonicalJson(head))) !== (yield* libSqlValue(() => canonicalJson(expectedHead)))) {
|
|
477
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The remote space head changed while reading its current snapshot.")));
|
|
478
|
+
}
|
|
479
|
+
const size = sizeResult.rows[0];
|
|
480
|
+
const provenanceOperations = size === undefined ? null : integer(rowValue(size, "provenance_count", 0));
|
|
481
|
+
const provenanceBytes = size === undefined ? null : integer(rowValue(size, "provenance_bytes", 1));
|
|
482
|
+
const recordCount = size === undefined ? null : integer(rowValue(size, "record_count", 2));
|
|
483
|
+
const recordBytes = size === undefined ? null : integer(rowValue(size, "record_bytes", 3));
|
|
484
|
+
const dependencyBytes = size === undefined ? null : integer(rowValue(size, "dependency_bytes", 4));
|
|
485
|
+
if (provenanceOperations === null || provenanceBytes === null || recordCount === null
|
|
486
|
+
|| recordBytes === null || dependencyBytes === null
|
|
487
|
+
|| provenanceOperations > OH_LIBSQL_STORE_LIMITS_V1.historyOperations
|
|
488
|
+
|| provenanceBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes
|
|
489
|
+
|| recordBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes
|
|
490
|
+
|| dependencyBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes) {
|
|
491
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("The current libSQL materialization exceeds its provider-safe response bounds.")));
|
|
492
|
+
}
|
|
493
|
+
if (recordResult.rows.length !== recordCount || provenanceResult.rows.length !== provenanceOperations) {
|
|
494
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The provider-safe snapshot queries omitted bounded authority rows.")));
|
|
495
|
+
}
|
|
496
|
+
const history = historyResult.rows[0];
|
|
497
|
+
const operationCount = history === undefined ? null : integer(rowValue(history, "count", 0));
|
|
498
|
+
const minimumValue = history === undefined ? undefined : rowValue(history, "minimum", 1);
|
|
499
|
+
const maximumValue = history === undefined ? undefined : rowValue(history, "maximum", 2);
|
|
500
|
+
const minimumSequence = history === undefined ? null : integer(rowValue(history, "minimum", 1));
|
|
501
|
+
const maximumSequence = history === undefined ? null : integer(rowValue(history, "maximum", 2));
|
|
502
|
+
if (operationCount !== head.sequence
|
|
503
|
+
|| (head.sequence === 0 && (minimumValue !== null || maximumValue !== null))
|
|
504
|
+
|| (head.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== head.sequence))) {
|
|
505
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote operation history does not exactly cover its current head.")));
|
|
506
|
+
}
|
|
507
|
+
if (recordResult.rows.length > maximumRecords) {
|
|
508
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("The remote graph exceeds the requested record snapshot bound.")));
|
|
509
|
+
}
|
|
510
|
+
const provenanceBySha256 = new Map<Sha256Hex, OhOperationV1>();
|
|
511
|
+
for (const row of provenanceResult.rows) {
|
|
512
|
+
const operation = (yield* libSqlValue(() => parseOperationRow(row, { spaceId: this.binding.spaceId })));
|
|
513
|
+
if (provenanceBySha256.has(operation.operationSha256)) {
|
|
514
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("A current materialization provenance operation is invalid.")));
|
|
515
|
+
}
|
|
516
|
+
provenanceBySha256.set(operation.operationSha256, operation);
|
|
517
|
+
}
|
|
518
|
+
if (provenanceBySha256.size !== provenanceOperations
|
|
519
|
+
|| (head.operationSha256 !== null && !provenanceBySha256.has(head.operationSha256))) {
|
|
520
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The current materialization omitted required provenance operations.")));
|
|
521
|
+
}
|
|
522
|
+
if (head.sequence > 0) {
|
|
523
|
+
const terminal = head.operationSha256 === null ? undefined : provenanceBySha256.get(head.operationSha256);
|
|
524
|
+
if (terminal === undefined || terminal.sequence !== head.sequence
|
|
525
|
+
|| terminal.graphRevisionSha256 !== head.graphRevisionSha256
|
|
526
|
+
|| terminal.recordsSha256 !== head.recordsSha256) {
|
|
527
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote space head differs from its terminal canonical operation.")));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const materialized = (yield* libSqlValue(() => recordResult.rows.map((row) => {
|
|
531
|
+
const json = rowValue(row, "record_json", 3);
|
|
532
|
+
if (typeof json !== "string") throw new OhIntegrityError("A materialized remote record is not JSON text.");
|
|
533
|
+
let value: unknown;
|
|
534
|
+
try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A materialized remote record is invalid."); }
|
|
535
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
536
|
+
const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 4));
|
|
537
|
+
const sequence = integer(rowValue(row, "sequence", 5));
|
|
538
|
+
if (record === null || canonicalJson(record) !== json || operationSha256 === null
|
|
539
|
+
|| sequence === null || sequence < 1 || sequence > head.sequence
|
|
540
|
+
|| rowValue(row, "record_key", 0) !== record.key
|
|
541
|
+
|| rowValue(row, "kind", 1) !== record.kind
|
|
542
|
+
|| rowValue(row, "record_sha256", 2) !== record.recordSha256) {
|
|
543
|
+
throw new OhIntegrityError("A materialized remote record is invalid.");
|
|
544
|
+
}
|
|
545
|
+
const provenance = provenanceBySha256.get(operationSha256);
|
|
546
|
+
if (provenance === undefined || provenance.sequence !== sequence
|
|
547
|
+
|| !provenance.changes.some((change) => change.kind === "put"
|
|
548
|
+
&& canonicalJson(change.record) === json)) {
|
|
549
|
+
throw new OhIntegrityError("A materialized remote record has no exact canonical provenance put.");
|
|
550
|
+
}
|
|
551
|
+
return { record, sequence };
|
|
552
|
+
})));
|
|
553
|
+
const records = (yield* libSqlValue(() => materialized.map(({ record }) => record)));
|
|
554
|
+
if ((yield* libSqlValue(() => canonicalSha256(records.map(knowledgeGraphRecordRefV1)))) !== head.recordsSha256) {
|
|
555
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("Materialized remote records do not reproduce the current head.")));
|
|
556
|
+
}
|
|
557
|
+
const dependencyRows = (yield* libSqlValue(() => dependencyResult.rows.map((row) => ({
|
|
558
|
+
dependency_key: rowValue(row, "dependency_key", 1),
|
|
559
|
+
record_key: rowValue(row, "record_key", 0),
|
|
560
|
+
}))));
|
|
561
|
+
const expectedDependencies = (yield* libSqlValue(() => records.flatMap((record) =>
|
|
562
|
+
record.dependencies.map((dependency) => ({ dependency_key: dependency, record_key: record.key })))));
|
|
563
|
+
if ((yield* libSqlValue(() => canonicalJson(dependencyRows))) !== (yield* libSqlValue(() => canonicalJson(expectedDependencies)))) {
|
|
564
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("Materialized remote dependencies do not match their record envelopes.")));
|
|
565
|
+
}
|
|
566
|
+
return { head, records, v: 1 };
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
snapshot(options: Readonly<{
|
|
571
|
+
head?: OhHeadRefV1;
|
|
572
|
+
maximumRecords?: number;
|
|
573
|
+
}> = {}): Effect.Effect<OhSnapshotV1, LibSqlFailure> {
|
|
574
|
+
return Effect.gen(this, function* () {
|
|
575
|
+
yield* libSqlValue(() => this.#assertOpen());
|
|
576
|
+
const current = yield* this.head();
|
|
577
|
+
const target = options.head === undefined ? current : yield* this.#headAt(options.head);
|
|
578
|
+
if (target.sequence > current.sequence) return yield* Effect.fail(libSqlFailure(new OhConflictError("The requested head is ahead of this space.")));
|
|
579
|
+
const maximumRecords = options.maximumRecords ?? OH_GRAPH_LIMITS_V1.recordsPerSnapshot;
|
|
580
|
+
if (!Number.isSafeInteger(maximumRecords) || maximumRecords < 1
|
|
581
|
+
|| maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
582
|
+
return yield* Effect.fail(libSqlFailure(new RangeError(`maximumRecords must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`)));
|
|
583
|
+
}
|
|
584
|
+
if (target.operationSha256 === current.operationSha256) {
|
|
585
|
+
return yield* this.#currentMaterializedSnapshot(current, maximumRecords);
|
|
586
|
+
}
|
|
587
|
+
if (target.sequence > OH_LIBSQL_STORE_LIMITS_V1.historyOperations) {
|
|
588
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("The requested libSQL history exceeds its operation replay bound.")));
|
|
589
|
+
}
|
|
590
|
+
const historyResults = yield* this.#client.batch([
|
|
591
|
+
{
|
|
592
|
+
sql: `SELECT count(*) AS count, min(operation.sequence) AS minimum,
|
|
593
|
+
max(operation.sequence) AS maximum,
|
|
594
|
+
coalesce(sum(length(CAST(operation.operation_json AS BLOB))), 0) AS canonical_bytes,
|
|
595
|
+
coalesce(sum(${OPERATION_RESPONSE_BYTES}), 0) AS response_bytes
|
|
596
|
+
FROM oh_authority_operations AS operation
|
|
597
|
+
WHERE operation.space_id = ? AND operation.sequence <= ?`,
|
|
598
|
+
args: [this.binding.spaceId, target.sequence]
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations AS operation
|
|
602
|
+
WHERE operation.space_id = ? AND operation.sequence <= ?
|
|
603
|
+
AND (SELECT coalesce(sum(length(CAST(candidate.operation_json AS BLOB))), 0)
|
|
604
|
+
FROM oh_authority_operations AS candidate
|
|
605
|
+
WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ?
|
|
606
|
+
AND (SELECT coalesce(sum(${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")}), 0)
|
|
607
|
+
FROM oh_authority_operations AS candidate
|
|
608
|
+
WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ?
|
|
609
|
+
ORDER BY operation.sequence`, args: [this.binding.spaceId, target.sequence,
|
|
610
|
+
this.binding.spaceId, target.sequence, OH_LIBSQL_STORE_LIMITS_V1.historyBytes,
|
|
611
|
+
this.binding.spaceId, target.sequence, OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes]
|
|
612
|
+
},
|
|
613
|
+
{ sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] },
|
|
614
|
+
], "read");
|
|
615
|
+
if (historyResults.length !== 3) {
|
|
616
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete history batch.")));
|
|
617
|
+
}
|
|
618
|
+
const [historySizeResult, historyRowResult, purgeResult] = historyResults as
|
|
619
|
+
[OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1];
|
|
620
|
+
const historySizeRow = historySizeResult.rows[0];
|
|
621
|
+
const historyBytes = historySizeRow === undefined ? null
|
|
622
|
+
: integer(rowValue(historySizeRow, "canonical_bytes", 3));
|
|
623
|
+
const responseBytes = historySizeRow === undefined ? null
|
|
624
|
+
: integer(rowValue(historySizeRow, "response_bytes", 4));
|
|
625
|
+
if (historyBytes === null || historyBytes > OH_LIBSQL_STORE_LIMITS_V1.historyBytes
|
|
626
|
+
|| responseBytes === null || responseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes) {
|
|
627
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("The requested libSQL history exceeds its provider-safe replay bounds.")));
|
|
628
|
+
}
|
|
629
|
+
const historyCount = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "count", 0));
|
|
630
|
+
const minimumSequence = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "minimum", 1));
|
|
631
|
+
const maximumSequence = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "maximum", 2));
|
|
632
|
+
if (historyCount !== target.sequence
|
|
633
|
+
|| (target.sequence === 0 && (rowValue(historySizeRow!, "minimum", 1) !== null
|
|
634
|
+
|| rowValue(historySizeRow!, "maximum", 2) !== null))
|
|
635
|
+
|| (target.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== target.sequence))
|
|
636
|
+
|| historyRowResult.rows.length !== historyCount) {
|
|
637
|
+
const purgeRow = purgeResult.rows[0];
|
|
638
|
+
if (purgeRow !== undefined) {
|
|
639
|
+
const purge = (yield* libSqlValue(() => parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256)));
|
|
640
|
+
this.#purged = purge;
|
|
641
|
+
return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError(purge)));
|
|
642
|
+
}
|
|
643
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote operation history does not exactly cover the requested head.")));
|
|
644
|
+
}
|
|
645
|
+
const operations = (yield* libSqlValue(() => historyRowResult.rows.map((row) => parseOperationRow(row,
|
|
646
|
+
{ spaceId: this.binding.spaceId }))));
|
|
647
|
+
const snapshot = (yield* libSqlValue(() => replayOhOperationsV1(this.binding.spaceId, operations, maximumRecords)));
|
|
648
|
+
if (snapshot.head.operationSha256 !== target.operationSha256
|
|
649
|
+
|| snapshot.head.recordsSha256 !== target.recordsSha256) {
|
|
650
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("Remote operation replay does not reproduce the requested head.")));
|
|
651
|
+
}
|
|
652
|
+
return snapshot;
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
changesSince(
|
|
657
|
+
fromValue: OhHeadRefV1,
|
|
658
|
+
options: Readonly<{ limit?: number; through?: OhHeadRefV1 }> = {},
|
|
659
|
+
): Effect.Effect<OhChangesPageV1, LibSqlFailure> {
|
|
660
|
+
return Effect.gen(this, function* () {
|
|
661
|
+
yield* libSqlValue(() => this.#assertOpen());
|
|
662
|
+
const from = (yield* libSqlValue(() => parseOhHeadRefV1(fromValue)));
|
|
663
|
+
if (from === null) return yield* Effect.fail(libSqlFailure(new TypeError("Invalid change-feed cursor.")));
|
|
664
|
+
const requestedThrough = options.through === undefined ? undefined : (yield* libSqlValue(() => parseOhHeadRefV1(options.through)));
|
|
665
|
+
if (requestedThrough === null) return yield* Effect.fail(libSqlFailure(new TypeError("Invalid change-feed through head.")));
|
|
666
|
+
const limit = (yield* libSqlValue(() => normalizeLimit(options.limit, OH_LIBSQL_STORE_LIMITS_V1.changeFeedLimit,
|
|
667
|
+
OH_LIBSQL_STORE_LIMITS_V1.changeFeedLimit)));
|
|
668
|
+
const throughSequence = requestedThrough?.sequence ?? null;
|
|
669
|
+
const results = yield* this.#client.batch([
|
|
670
|
+
{
|
|
671
|
+
sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
672
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId]
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
676
|
+
WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, from.sequence]
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
680
|
+
WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, throughSequence]
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
sql: `SELECT count(*) AS count, coalesce(sum(response_bytes), 0) AS response_bytes FROM (
|
|
684
|
+
SELECT ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS response_bytes
|
|
685
|
+
FROM oh_authority_operations AS candidate
|
|
686
|
+
WHERE candidate.space_id = ? AND candidate.sequence > ?
|
|
687
|
+
AND candidate.sequence <= coalesce(?,
|
|
688
|
+
(SELECT sequence FROM oh_authority_spaces WHERE space_id = ?))
|
|
689
|
+
ORDER BY candidate.sequence LIMIT ?
|
|
690
|
+
)`, args: [this.binding.spaceId, from.sequence, throughSequence,
|
|
691
|
+
this.binding.spaceId, limit + 1]
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
695
|
+
AS operation WHERE operation.space_id = ? AND operation.sequence > ?
|
|
696
|
+
AND operation.sequence <= coalesce(?,
|
|
697
|
+
(SELECT sequence FROM oh_authority_spaces WHERE space_id = ?))
|
|
698
|
+
AND (SELECT coalesce(sum(response_bytes), 0) FROM (
|
|
699
|
+
SELECT ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS response_bytes
|
|
700
|
+
FROM oh_authority_operations AS candidate
|
|
701
|
+
WHERE candidate.space_id = ? AND candidate.sequence > ?
|
|
702
|
+
AND candidate.sequence <= coalesce(?,
|
|
703
|
+
(SELECT sequence FROM oh_authority_spaces WHERE space_id = ?))
|
|
704
|
+
ORDER BY candidate.sequence LIMIT ?
|
|
705
|
+
)) <= ?
|
|
706
|
+
ORDER BY operation.sequence LIMIT ?`,
|
|
707
|
+
args: [this.binding.spaceId, from.sequence, throughSequence, this.binding.spaceId,
|
|
708
|
+
this.binding.spaceId, from.sequence, throughSequence, this.binding.spaceId, limit + 1,
|
|
709
|
+
OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes, limit + 1]
|
|
710
|
+
},
|
|
711
|
+
{ sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] },
|
|
712
|
+
], "read");
|
|
713
|
+
if (results.length !== 6) return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete change-feed batch.")));
|
|
714
|
+
const [currentResult, fromResult, throughResult, pageSizeResult, pageResult, purgeResult] = results as
|
|
715
|
+
[OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1,
|
|
716
|
+
OhLibSqlResultV1, OhLibSqlResultV1];
|
|
717
|
+
const currentRow = currentResult.rows[0];
|
|
718
|
+
if (currentRow === undefined) {
|
|
719
|
+
const purgeRow = purgeResult.rows[0];
|
|
720
|
+
if (purgeRow !== undefined) {
|
|
721
|
+
const purge = (yield* libSqlValue(() => parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256)));
|
|
722
|
+
this.#purged = purge;
|
|
723
|
+
return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError(purge)));
|
|
724
|
+
}
|
|
725
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote Oh space disappeared while reading its change feed.")));
|
|
726
|
+
}
|
|
727
|
+
const current = (yield* libSqlValue(() => parseHeadRow(currentRow)));
|
|
728
|
+
const resolveHead = (reference: OhHeadRefV1, result: OhLibSqlResultV1): OhHeadV1 => {
|
|
729
|
+
if (reference.sequence === 0) return emptyOhHeadV1();
|
|
730
|
+
const row = result.rows[0];
|
|
731
|
+
if (row === undefined) throw new OhConflictError("A requested change-feed head is not present in this space.");
|
|
732
|
+
const operation = parseOperationRow(row, { spaceId: this.binding.spaceId });
|
|
733
|
+
if (operation.spaceId !== this.binding.spaceId || operation.sequence !== reference.sequence
|
|
734
|
+
|| operation.operationSha256 !== reference.operationSha256) {
|
|
735
|
+
throw new OhConflictError("A requested change-feed sequence identifies a different operation head.");
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
generation: operation.sequence, graphRevisionSha256: operation.graphRevisionSha256,
|
|
739
|
+
operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256,
|
|
740
|
+
sequence: operation.sequence, v: 1
|
|
741
|
+
};
|
|
742
|
+
};
|
|
743
|
+
const fromHead = yield* libSqlValue(() => resolveHead(from, fromResult));
|
|
744
|
+
const through = requestedThrough === undefined ? current : yield* libSqlValue(() => resolveHead(requestedThrough, throughResult));
|
|
745
|
+
if (fromHead.sequence > through.sequence || through.sequence > current.sequence) {
|
|
746
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The change-feed bounds do not identify one remote history prefix.")));
|
|
747
|
+
}
|
|
748
|
+
const pageSizeRow = pageSizeResult.rows[0];
|
|
749
|
+
const pageCount = pageSizeRow === undefined ? null : integer(rowValue(pageSizeRow, "count", 0));
|
|
750
|
+
const pageResponseBytes = pageSizeRow === undefined ? null
|
|
751
|
+
: integer(rowValue(pageSizeRow, "response_bytes", 1));
|
|
752
|
+
if (pageCount === null || pageCount > limit + 1 || pageResponseBytes === null) {
|
|
753
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote change feed returned invalid response bounds.")));
|
|
754
|
+
}
|
|
755
|
+
if (pageResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes) {
|
|
756
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("The requested change-feed page exceeds its provider response bound.")));
|
|
757
|
+
}
|
|
758
|
+
const parsed = (yield* libSqlValue(() => pageResult.rows.map((row) => parseOperationRow(row, { spaceId: this.binding.spaceId }))));
|
|
759
|
+
if (parsed.length !== pageCount) {
|
|
760
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote change feed omitted provider-bounded rows.")));
|
|
761
|
+
}
|
|
762
|
+
const hasMore = parsed.length > limit;
|
|
763
|
+
const operations = parsed.slice(0, limit);
|
|
764
|
+
let prior: OhHeadRefV1 = fromHead;
|
|
765
|
+
for (const operation of parsed) {
|
|
766
|
+
if (operation.spaceId !== this.binding.spaceId
|
|
767
|
+
|| operation.sequence !== prior.sequence + 1
|
|
768
|
+
|| operation.parentOperationSha256 !== prior.operationSha256) {
|
|
769
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote change feed contains a gap or fork.")));
|
|
770
|
+
}
|
|
771
|
+
prior = { operationSha256: operation.operationSha256, sequence: operation.sequence };
|
|
772
|
+
}
|
|
773
|
+
if (!hasMore && (prior.sequence !== through.sequence
|
|
774
|
+
|| prior.operationSha256 !== through.operationSha256)) {
|
|
775
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote change feed does not reach its pinned through head.")));
|
|
776
|
+
}
|
|
777
|
+
const last = operations.at(-1);
|
|
778
|
+
const to = last === undefined
|
|
779
|
+
? { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence }
|
|
780
|
+
: { operationSha256: last.operationSha256, sequence: last.sequence };
|
|
781
|
+
return {
|
|
782
|
+
from: { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence },
|
|
783
|
+
hasMore, operations, through, to, v: 1
|
|
784
|
+
};
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
#assertMaterializedSnapshot(snapshot: OhSnapshotV1): Effect.Effect<void, LibSqlFailure> {
|
|
789
|
+
return Effect.gen(this, function* () {
|
|
790
|
+
if (snapshot.head.sequence > OH_LIBSQL_STORE_LIMITS_V1.historyOperations) {
|
|
791
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("The libSQL authority exceeds its explicit verification operation bound.")));
|
|
792
|
+
}
|
|
793
|
+
const verificationResults = yield* this.#client.batch([
|
|
794
|
+
{
|
|
795
|
+
sql: `SELECT * FROM (WITH bounded_operation AS (
|
|
796
|
+
SELECT * FROM oh_authority_operations
|
|
797
|
+
WHERE space_id = ? AND sequence <= ?
|
|
798
|
+
) SELECT count(*) AS operation_count, min(operation.sequence) AS minimum,
|
|
799
|
+
max(operation.sequence) AS maximum,
|
|
800
|
+
coalesce(sum(length(CAST(operation.operation_json AS BLOB))), 0) AS canonical_bytes,
|
|
801
|
+
coalesce(sum(${OPERATION_RESPONSE_BYTES}), 0) AS operation_response_bytes,
|
|
802
|
+
(SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0)
|
|
803
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?) AS record_response_bytes,
|
|
804
|
+
(SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0)
|
|
805
|
+
FROM oh_authority_dependencies AS dependency
|
|
806
|
+
WHERE dependency.space_id = ?) AS dependency_response_bytes,
|
|
807
|
+
(SELECT coalesce(sum(${OPERATION_RECORD_RESPONSE_BYTES}), 0)
|
|
808
|
+
FROM oh_authority_operation_records AS materialized
|
|
809
|
+
JOIN bounded_operation AS owner
|
|
810
|
+
ON owner.operation_sha256 = materialized.operation_sha256) AS operation_record_response_bytes
|
|
811
|
+
FROM bounded_operation AS operation)`, args: [this.binding.spaceId, snapshot.head.sequence,
|
|
812
|
+
this.binding.spaceId, this.binding.spaceId]
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS}
|
|
816
|
+
FROM oh_authority_operations AS operation
|
|
817
|
+
WHERE operation.space_id = ? AND operation.sequence <= ?
|
|
818
|
+
AND (SELECT coalesce(sum(length(CAST(candidate.operation_json AS BLOB))), 0)
|
|
819
|
+
FROM oh_authority_operations AS candidate
|
|
820
|
+
WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ?
|
|
821
|
+
AND (SELECT coalesce(sum(${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")}), 0)
|
|
822
|
+
FROM oh_authority_operations AS candidate
|
|
823
|
+
WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ?
|
|
824
|
+
ORDER BY operation.sequence`, args: [this.binding.spaceId, snapshot.head.sequence,
|
|
825
|
+
this.binding.spaceId, snapshot.head.sequence, OH_LIBSQL_STORE_LIMITS_V1.historyBytes,
|
|
826
|
+
this.binding.spaceId, snapshot.head.sequence, OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes]
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
sql: `SELECT record_key, kind, record_sha256, record_json, operation_sha256, sequence
|
|
830
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?
|
|
831
|
+
AND (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0)
|
|
832
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?) <= ?
|
|
833
|
+
ORDER BY record.record_key`, args: [this.binding.spaceId, this.binding.spaceId,
|
|
834
|
+
OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes]
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
sql: `SELECT record_key, dependency_key FROM oh_authority_dependencies AS dependency
|
|
838
|
+
WHERE dependency.space_id = ?
|
|
839
|
+
AND (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0)
|
|
840
|
+
FROM oh_authority_dependencies AS dependency WHERE dependency.space_id = ?) <= ?
|
|
841
|
+
ORDER BY dependency.record_key, dependency.dependency_key`, args: [this.binding.spaceId,
|
|
842
|
+
this.binding.spaceId, OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes]
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
sql: `SELECT materialized.space_id, materialized.operation_sha256, materialized.ordinal,
|
|
846
|
+
materialized.record_key, materialized.change_kind, materialized.record_sha256
|
|
847
|
+
FROM oh_authority_operation_records AS materialized
|
|
848
|
+
JOIN oh_authority_operations AS operation
|
|
849
|
+
ON operation.operation_sha256 = materialized.operation_sha256
|
|
850
|
+
WHERE operation.space_id = ? AND operation.sequence <= ?
|
|
851
|
+
AND (SELECT coalesce(sum(${OPERATION_RECORD_RESPONSE_BYTES}), 0)
|
|
852
|
+
FROM oh_authority_operation_records AS materialized
|
|
853
|
+
JOIN oh_authority_operations AS owner
|
|
854
|
+
ON owner.operation_sha256 = materialized.operation_sha256
|
|
855
|
+
WHERE owner.space_id = ? AND owner.sequence <= ?) <= ?
|
|
856
|
+
ORDER BY operation.sequence, materialized.ordinal`, args: [this.binding.spaceId,
|
|
857
|
+
snapshot.head.sequence, this.binding.spaceId, snapshot.head.sequence,
|
|
858
|
+
OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes]
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
862
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId]
|
|
863
|
+
},
|
|
864
|
+
], "read");
|
|
865
|
+
if (verificationResults.length !== 6) {
|
|
866
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete verification batch.")));
|
|
867
|
+
}
|
|
868
|
+
const [sizeResult, operationResult, recordResult, dependencyResult, operationRecordResult, headResult] =
|
|
869
|
+
verificationResults as [OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1,
|
|
870
|
+
OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1];
|
|
871
|
+
const sizeRow = sizeResult.rows[0];
|
|
872
|
+
const operationCount = sizeRow === undefined ? null : integer(rowValue(sizeRow, "operation_count", 0));
|
|
873
|
+
const minimumValue = sizeRow === undefined ? undefined : rowValue(sizeRow, "minimum", 1);
|
|
874
|
+
const maximumValue = sizeRow === undefined ? undefined : rowValue(sizeRow, "maximum", 2);
|
|
875
|
+
const minimumSequence = sizeRow === undefined ? null : integer(rowValue(sizeRow, "minimum", 1));
|
|
876
|
+
const maximumSequence = sizeRow === undefined ? null : integer(rowValue(sizeRow, "maximum", 2));
|
|
877
|
+
const historyBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "canonical_bytes", 3));
|
|
878
|
+
const operationResponseBytes = sizeRow === undefined ? null
|
|
879
|
+
: integer(rowValue(sizeRow, "operation_response_bytes", 4));
|
|
880
|
+
const recordResponseBytes = sizeRow === undefined ? null
|
|
881
|
+
: integer(rowValue(sizeRow, "record_response_bytes", 5));
|
|
882
|
+
const dependencyResponseBytes = sizeRow === undefined ? null
|
|
883
|
+
: integer(rowValue(sizeRow, "dependency_response_bytes", 6));
|
|
884
|
+
const operationRecordResponseBytes = sizeRow === undefined ? null
|
|
885
|
+
: integer(rowValue(sizeRow, "operation_record_response_bytes", 7));
|
|
886
|
+
if (historyBytes === null || historyBytes > OH_LIBSQL_STORE_LIMITS_V1.historyBytes
|
|
887
|
+
|| operationResponseBytes === null
|
|
888
|
+
|| operationResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes
|
|
889
|
+
|| recordResponseBytes === null
|
|
890
|
+
|| recordResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes
|
|
891
|
+
|| dependencyResponseBytes === null
|
|
892
|
+
|| dependencyResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes
|
|
893
|
+
|| operationRecordResponseBytes === null
|
|
894
|
+
|| operationRecordResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes) {
|
|
895
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("The libSQL authority exceeds its provider-safe verification bounds.")));
|
|
896
|
+
}
|
|
897
|
+
if (operationCount !== snapshot.head.sequence
|
|
898
|
+
|| (snapshot.head.sequence === 0 && (minimumValue !== null || maximumValue !== null))
|
|
899
|
+
|| (snapshot.head.sequence > 0
|
|
900
|
+
&& (minimumSequence !== 1 || maximumSequence !== snapshot.head.sequence))
|
|
901
|
+
|| operationResult.rows.length !== operationCount) {
|
|
902
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote operation history does not exactly cover its verified head.")));
|
|
903
|
+
}
|
|
904
|
+
const headRow = headResult.rows[0];
|
|
905
|
+
if (headRow === undefined) return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority lost its head during verification.")));
|
|
906
|
+
if ((yield* libSqlValue(() => canonicalJson(parseHeadRow(headRow)))) !== (yield* libSqlValue(() => canonicalJson(snapshot.head)))) {
|
|
907
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The remote authority head changed during verification.")));
|
|
908
|
+
}
|
|
909
|
+
const operations = (yield* libSqlValue(() => operationResult.rows.map((row) => {
|
|
910
|
+
return parseOperationRow(row, { spaceId: this.binding.spaceId });
|
|
911
|
+
})));
|
|
912
|
+
const replayed = (yield* libSqlValue(() => replayOhOperationsV1(this.binding.spaceId, operations)));
|
|
913
|
+
if ((yield* libSqlValue(() => canonicalJson(replayed))) !== (yield* libSqlValue(() => canonicalJson(snapshot)))) {
|
|
914
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("Remote operation replay changed during materialization verification.")));
|
|
915
|
+
}
|
|
916
|
+
const materializedBy = new Map<string, Readonly<{ operationSha256: Sha256Hex; sequence: number }>>();
|
|
917
|
+
for (const operation of operations) {
|
|
918
|
+
for (const change of operation.changes) {
|
|
919
|
+
const key = change.kind === "put" ? change.record.key : change.key;
|
|
920
|
+
if (change.kind === "put") materializedBy.set(key,
|
|
921
|
+
{ operationSha256: operation.operationSha256, sequence: operation.sequence });
|
|
922
|
+
else materializedBy.delete(key);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const records: KnowledgeGraphRecordV1[] = (yield* libSqlValue(() => recordResult.rows.map((row) => {
|
|
926
|
+
const json = rowValue(row, "record_json", 3);
|
|
927
|
+
if (typeof json !== "string") throw new OhIntegrityError("A materialized remote record is not JSON text.");
|
|
928
|
+
let value: unknown;
|
|
929
|
+
try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A materialized remote record is invalid."); }
|
|
930
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
931
|
+
const provenance = record === null ? undefined : materializedBy.get(record.key);
|
|
932
|
+
if (record === null || canonicalJson(record) !== json
|
|
933
|
+
|| rowValue(row, "record_key", 0) !== record.key
|
|
934
|
+
|| rowValue(row, "kind", 1) !== record.kind
|
|
935
|
+
|| rowValue(row, "record_sha256", 2) !== record.recordSha256
|
|
936
|
+
|| provenance === undefined
|
|
937
|
+
|| rowValue(row, "operation_sha256", 4) !== provenance.operationSha256
|
|
938
|
+
|| integer(rowValue(row, "sequence", 5)) !== provenance.sequence) {
|
|
939
|
+
throw new OhIntegrityError("A materialized remote record differs from operation replay.");
|
|
940
|
+
}
|
|
941
|
+
return record;
|
|
942
|
+
})));
|
|
943
|
+
if ((yield* libSqlValue(() => canonicalJson(records))) !== (yield* libSqlValue(() => canonicalJson(snapshot.records)))) {
|
|
944
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("Remote materialized records do not match operation replay.")));
|
|
945
|
+
}
|
|
946
|
+
const dependencyRows = (yield* libSqlValue(() => dependencyResult.rows.map((row) => ({
|
|
947
|
+
dependency_key: rowValue(row, "dependency_key", 1),
|
|
948
|
+
record_key: rowValue(row, "record_key", 0),
|
|
949
|
+
}))));
|
|
950
|
+
const expectedDependencies = (yield* libSqlValue(() => snapshot.records.flatMap((record) =>
|
|
951
|
+
record.dependencies.map((dependency) => ({ dependency_key: dependency, record_key: record.key })))));
|
|
952
|
+
if ((yield* libSqlValue(() => canonicalJson(dependencyRows))) !== (yield* libSqlValue(() => canonicalJson(expectedDependencies)))) {
|
|
953
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("Remote materialized dependencies do not match operation replay.")));
|
|
954
|
+
}
|
|
955
|
+
const operationRecordRows = (yield* libSqlValue(() => operationRecordResult.rows.map((row) => ({
|
|
956
|
+
change_kind: rowValue(row, "change_kind", 4),
|
|
957
|
+
operation_sha256: rowValue(row, "operation_sha256", 1),
|
|
958
|
+
ordinal: integer(rowValue(row, "ordinal", 2)),
|
|
959
|
+
record_key: rowValue(row, "record_key", 3),
|
|
960
|
+
record_sha256: rowValue(row, "record_sha256", 5),
|
|
961
|
+
space_id: rowValue(row, "space_id", 0),
|
|
962
|
+
}))));
|
|
963
|
+
const expectedOperationRecords = (yield* libSqlValue(() => operations.flatMap((operation) =>
|
|
964
|
+
operation.changes.map((change, ordinal) => ({
|
|
965
|
+
change_kind: change.kind,
|
|
966
|
+
operation_sha256: operation.operationSha256, ordinal,
|
|
967
|
+
record_key: change.kind === "put" ? change.record.key : change.key,
|
|
968
|
+
record_sha256: change.kind === "put" ? change.record.recordSha256 : change.priorSha256,
|
|
969
|
+
space_id: this.binding.spaceId
|
|
970
|
+
})))));
|
|
971
|
+
if ((yield* libSqlValue(() => canonicalJson(operationRecordRows))) !== (yield* libSqlValue(() => canonicalJson(expectedOperationRecords)))) {
|
|
972
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("Remote operation-record rows do not match operation replay.")));
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
#operationById(operationId: string): Effect.Effect<OhOperationV1 | null, LibSqlFailure> {
|
|
978
|
+
return Effect.gen(this, function* () {
|
|
979
|
+
const row = yield* queryOne(this.#client, {
|
|
980
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
981
|
+
WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId]
|
|
982
|
+
});
|
|
983
|
+
if (row === null) return null;
|
|
984
|
+
return (yield* libSqlValue(() => parseOperationRow(row, { operationId, spaceId: this.binding.spaceId })));
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
#commitPreflight(operationId: string): Effect.Effect<Readonly<{
|
|
989
|
+
current: OhHeadV1;
|
|
990
|
+
duplicate: OhOperationV1 | null;
|
|
991
|
+
}>, LibSqlFailure> {
|
|
992
|
+
return Effect.gen(this, function* () {
|
|
993
|
+
const results = yield* this.#client.batch([
|
|
994
|
+
{
|
|
995
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
996
|
+
WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId]
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
1000
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId]
|
|
1001
|
+
},
|
|
1002
|
+
{ sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] },
|
|
1003
|
+
], "read");
|
|
1004
|
+
if (results.length !== 3) return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete commit preflight.")));
|
|
1005
|
+
const [duplicateResult, headResult, purgeResult] = results as
|
|
1006
|
+
[OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1];
|
|
1007
|
+
const headRow = headResult.rows[0];
|
|
1008
|
+
if (headRow === undefined) {
|
|
1009
|
+
const purgeRow = purgeResult.rows[0];
|
|
1010
|
+
if (purgeRow !== undefined) {
|
|
1011
|
+
const purge = (yield* libSqlValue(() => parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256)));
|
|
1012
|
+
this.#purged = purge;
|
|
1013
|
+
return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError(purge)));
|
|
1014
|
+
}
|
|
1015
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote space disappeared during commit preflight.")));
|
|
1016
|
+
}
|
|
1017
|
+
const duplicateRow = duplicateResult.rows[0];
|
|
1018
|
+
return {
|
|
1019
|
+
current: (yield* libSqlValue(() => parseHeadRow(headRow))), duplicate: duplicateRow === undefined ? null
|
|
1020
|
+
: (yield* libSqlValue(() => parseOperationRow(duplicateRow, { operationId, spaceId: this.binding.spaceId })))
|
|
1021
|
+
};
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
#assertOperationReachable(operation: OhOperationV1, expectedHead: OhHeadV1): Effect.Effect<void, LibSqlFailure> {
|
|
1026
|
+
return Effect.gen(this, function* () {
|
|
1027
|
+
if (operation.sequence < 1 || operation.sequence > expectedHead.sequence) {
|
|
1028
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("A remote idempotent operation is not reachable from the current head.")));
|
|
1029
|
+
}
|
|
1030
|
+
const results = yield* this.#client.batch([
|
|
1031
|
+
{
|
|
1032
|
+
sql: `SELECT * FROM (WITH RECURSIVE authority_chain(sequence, operation_sha256) AS (
|
|
1033
|
+
SELECT sequence, operation_sha256 FROM oh_authority_operations
|
|
1034
|
+
WHERE space_id = ? AND sequence = ? AND operation_sha256 = ?
|
|
1035
|
+
UNION ALL
|
|
1036
|
+
SELECT candidate.sequence, candidate.operation_sha256
|
|
1037
|
+
FROM oh_authority_operations AS candidate
|
|
1038
|
+
JOIN authority_chain AS prior
|
|
1039
|
+
ON candidate.space_id = ? AND candidate.sequence = prior.sequence + 1
|
|
1040
|
+
AND candidate.parent_operation_sha256 = prior.operation_sha256
|
|
1041
|
+
WHERE candidate.sequence <= ?
|
|
1042
|
+
) SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum,
|
|
1043
|
+
(SELECT operation_sha256 FROM authority_chain ORDER BY sequence DESC LIMIT 1) AS terminal_sha256
|
|
1044
|
+
FROM authority_chain)`, args: [this.binding.spaceId, operation.sequence,
|
|
1045
|
+
operation.operationSha256, this.binding.spaceId, expectedHead.sequence]
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
1049
|
+
WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, expectedHead.sequence]
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
1053
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId]
|
|
1054
|
+
},
|
|
1055
|
+
{ sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] },
|
|
1056
|
+
], "read");
|
|
1057
|
+
if (results.length !== 4) return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete reachability proof.")));
|
|
1058
|
+
const [chainResult, terminalResult, headResult, purgeResult] = results as
|
|
1059
|
+
[OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1];
|
|
1060
|
+
const headRow = headResult.rows[0];
|
|
1061
|
+
if (headRow === undefined) {
|
|
1062
|
+
const purgeRow = purgeResult.rows[0];
|
|
1063
|
+
if (purgeRow !== undefined) {
|
|
1064
|
+
const purge = (yield* libSqlValue(() => parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256)));
|
|
1065
|
+
this.#purged = purge;
|
|
1066
|
+
return yield* Effect.fail(libSqlFailure(new OhPurgedSpaceError(purge)));
|
|
1067
|
+
}
|
|
1068
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote space disappeared during an idempotency proof.")));
|
|
1069
|
+
}
|
|
1070
|
+
const current = (yield* libSqlValue(() => parseHeadRow(headRow)));
|
|
1071
|
+
if ((yield* libSqlValue(() => canonicalJson(current))) !== (yield* libSqlValue(() => canonicalJson(expectedHead)))) {
|
|
1072
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The remote space head changed during an idempotency proof.")));
|
|
1073
|
+
}
|
|
1074
|
+
const chain = chainResult.rows[0];
|
|
1075
|
+
const count = chain === undefined ? null : integer(rowValue(chain, "count", 0));
|
|
1076
|
+
const minimum = chain === undefined ? null : integer(rowValue(chain, "minimum", 1));
|
|
1077
|
+
const maximum = chain === undefined ? null : integer(rowValue(chain, "maximum", 2));
|
|
1078
|
+
const terminalSha256 = chain === undefined ? null : rowValue(chain, "terminal_sha256", 3);
|
|
1079
|
+
if (count !== expectedHead.sequence - operation.sequence + 1
|
|
1080
|
+
|| minimum !== operation.sequence || maximum !== expectedHead.sequence
|
|
1081
|
+
|| terminalSha256 !== expectedHead.operationSha256) {
|
|
1082
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("A remote idempotent operation has no exact path to the current head.")));
|
|
1083
|
+
}
|
|
1084
|
+
const terminalRow = terminalResult.rows[0];
|
|
1085
|
+
const expectedOperationSha256 = expectedHead.operationSha256;
|
|
1086
|
+
if (terminalRow === undefined || expectedOperationSha256 === null) {
|
|
1087
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote current head operation is missing.")));
|
|
1088
|
+
}
|
|
1089
|
+
const terminal = (yield* libSqlValue(() => parseOperationRow(terminalRow, {
|
|
1090
|
+
operationSha256: expectedOperationSha256,
|
|
1091
|
+
spaceId: this.binding.spaceId
|
|
1092
|
+
})));
|
|
1093
|
+
if (terminal.sequence !== expectedHead.sequence
|
|
1094
|
+
|| terminal.graphRevisionSha256 !== expectedHead.graphRevisionSha256
|
|
1095
|
+
|| terminal.recordsSha256 !== expectedHead.recordsSha256) {
|
|
1096
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote space head differs from its terminal canonical operation.")));
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
commit(input: OhCommitInputV1): Effect.Effect<OhOperationV1, LibSqlFailure> {
|
|
1102
|
+
return Effect.gen(this, function* () {
|
|
1103
|
+
yield* libSqlValue(() => this.#assertOpen());
|
|
1104
|
+
const actorId = (yield* libSqlValue(() => safeCode(input.actorId)));
|
|
1105
|
+
const operationId = (yield* libSqlValue(() => safeCode(input.operationId)));
|
|
1106
|
+
const maximumOperationBytes = input.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
|
|
1107
|
+
const changes = (yield* libSqlValue(() => canonicalKnowledgeGraphChangesV1(input.changes)));
|
|
1108
|
+
if (actorId === null || operationId === null || changes.length === 0
|
|
1109
|
+
|| !Number.isSafeInteger(maximumOperationBytes)
|
|
1110
|
+
|| maximumOperationBytes < 1
|
|
1111
|
+
|| maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
|
|
1112
|
+
return yield* Effect.fail(libSqlFailure(new TypeError("Invalid Oh commit input.")));
|
|
1113
|
+
}
|
|
1114
|
+
if (changes.length > OH_LIBSQL_STORE_LIMITS_V1.changesPerCommit) {
|
|
1115
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("A direct libSQL commit exceeds its change-count bound.")));
|
|
1116
|
+
}
|
|
1117
|
+
const dependencies = (yield* libSqlValue(() => changes.reduce((count, change) => count
|
|
1118
|
+
+ (change.kind === "put" ? change.record.dependencies.length : 0), 0)));
|
|
1119
|
+
if (dependencies > OH_LIBSQL_STORE_LIMITS_V1.dependenciesPerCommit) {
|
|
1120
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("A direct libSQL commit exceeds its dependency-count bound.")));
|
|
1121
|
+
}
|
|
1122
|
+
const { current, duplicate } = yield* this.#commitPreflight(operationId);
|
|
1123
|
+
if (duplicate !== null) {
|
|
1124
|
+
yield* this.#assertOperationReachable(duplicate, current);
|
|
1125
|
+
if (duplicate.actorId !== actorId || (yield* libSqlValue(() => canonicalJson(duplicate.changes))) !== (yield* libSqlValue(() => canonicalJson(changes)))) {
|
|
1126
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The operation ID is already bound to different content.")));
|
|
1127
|
+
}
|
|
1128
|
+
const operationBytes = utf8ByteLength((yield* libSqlValue(() => canonicalJson(duplicate))));
|
|
1129
|
+
if (operationBytes > maximumOperationBytes) {
|
|
1130
|
+
return yield* Effect.fail(libSqlFailure(new OhOperationSizeError(operationBytes, maximumOperationBytes)));
|
|
1131
|
+
}
|
|
1132
|
+
return duplicate;
|
|
1133
|
+
}
|
|
1134
|
+
if (!Number.isSafeInteger(input.expectedHead.generation) || input.expectedHead.generation < 0
|
|
1135
|
+
|| current.generation !== input.expectedHead.generation
|
|
1136
|
+
|| current.operationSha256 !== input.expectedHead.operationSha256) {
|
|
1137
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The expected head does not match the current remote space head.")));
|
|
1138
|
+
}
|
|
1139
|
+
const snapshot = yield* this.#currentMaterializedSnapshot(current, OH_GRAPH_LIMITS_V1.recordsPerSnapshot);
|
|
1140
|
+
const instant = (yield* libSqlValue(() => input.instant)) ?? (yield* this.#client.currentInstant);
|
|
1141
|
+
const transition = (yield* libSqlValue(() => transitionOhSnapshotV1({
|
|
1142
|
+
actorId, changes,
|
|
1143
|
+
instant, maximumOperationBytes,
|
|
1144
|
+
operationId, snapshot, spaceId: this.binding.spaceId
|
|
1145
|
+
})));
|
|
1146
|
+
const operation = transition.operation;
|
|
1147
|
+
const operationJson = (yield* libSqlValue(() => canonicalJson(operation)));
|
|
1148
|
+
const operationBytes = utf8ByteLength(operationJson);
|
|
1149
|
+
if (operationBytes > maximumOperationBytes) {
|
|
1150
|
+
return yield* Effect.fail(libSqlFailure(new OhOperationSizeError(operationBytes, maximumOperationBytes)));
|
|
1151
|
+
}
|
|
1152
|
+
if (operationBytes > OH_LIBSQL_STORE_LIMITS_V1.operationBytes) {
|
|
1153
|
+
return yield* Effect.fail(libSqlFailure(new RangeError("A direct libSQL operation exceeds its canonical byte bound.")));
|
|
1154
|
+
}
|
|
1155
|
+
const existsOperation = "EXISTS (SELECT 1 FROM oh_authority_operations WHERE operation_sha256 = ?)";
|
|
1156
|
+
const statements: OhLibSqlStatementV1[] = [{
|
|
1157
|
+
sql: `INSERT INTO oh_authority_operations(operation_sha256, space_id, sequence,
|
|
1158
|
+
operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256,
|
|
1159
|
+
operation_json, instant)
|
|
1160
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
1161
|
+
WHERE EXISTS (SELECT 1 FROM oh_authority_spaces
|
|
1162
|
+
WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?)`,
|
|
1163
|
+
args: [operation.operationSha256, this.binding.spaceId, operation.sequence, operation.operationId,
|
|
1164
|
+
operation.parentOperationSha256, operation.graphRevisionSha256, operation.recordsSha256,
|
|
1165
|
+
operationJson, operation.instant, this.binding.spaceId, current.generation,
|
|
1166
|
+
current.operationSha256],
|
|
1167
|
+
}];
|
|
1168
|
+
for (const [ordinal, change] of operation.changes.entries()) {
|
|
1169
|
+
const key = change.kind === "put" ? change.record.key : change.key;
|
|
1170
|
+
const digest = change.kind === "put" ? change.record.recordSha256 : change.priorSha256;
|
|
1171
|
+
statements.push({
|
|
1172
|
+
sql: `INSERT INTO oh_authority_operation_records(space_id, operation_sha256,
|
|
1173
|
+
ordinal, record_key, change_kind, record_sha256)
|
|
1174
|
+
SELECT ?, ?, ?, ?, ?, ? WHERE ${existsOperation}`,
|
|
1175
|
+
args: [this.binding.spaceId, operation.operationSha256, ordinal, key, change.kind, digest,
|
|
1176
|
+
operation.operationSha256]
|
|
1177
|
+
});
|
|
1178
|
+
statements.push({
|
|
1179
|
+
sql: `DELETE FROM oh_authority_dependencies WHERE space_id = ? AND record_key = ?
|
|
1180
|
+
AND ${existsOperation}`, args: [this.binding.spaceId, key, operation.operationSha256]
|
|
1181
|
+
});
|
|
1182
|
+
if (change.kind === "put") {
|
|
1183
|
+
statements.push({
|
|
1184
|
+
sql: `INSERT INTO oh_authority_records(space_id, record_key, kind,
|
|
1185
|
+
record_sha256, record_json, operation_sha256, sequence)
|
|
1186
|
+
SELECT ?, ?, ?, ?, ?, ?, ? WHERE ${existsOperation}
|
|
1187
|
+
ON CONFLICT(space_id, record_key) DO UPDATE SET kind = excluded.kind,
|
|
1188
|
+
record_sha256 = excluded.record_sha256, record_json = excluded.record_json,
|
|
1189
|
+
operation_sha256 = excluded.operation_sha256, sequence = excluded.sequence`,
|
|
1190
|
+
args: [this.binding.spaceId, key, change.record.kind, change.record.recordSha256,
|
|
1191
|
+
(yield* libSqlValue(() => canonicalJson(change.record))), operation.operationSha256, operation.sequence,
|
|
1192
|
+
operation.operationSha256]
|
|
1193
|
+
});
|
|
1194
|
+
} else {
|
|
1195
|
+
statements.push({
|
|
1196
|
+
sql: `DELETE FROM oh_authority_records WHERE space_id = ? AND record_key = ?
|
|
1197
|
+
AND record_sha256 = ? AND ${existsOperation}`,
|
|
1198
|
+
args: [this.binding.spaceId, key, change.priorSha256, operation.operationSha256]
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
for (const change of operation.changes) {
|
|
1203
|
+
if (change.kind !== "put") continue;
|
|
1204
|
+
for (const dependency of change.record.dependencies) {
|
|
1205
|
+
statements.push({
|
|
1206
|
+
sql: `INSERT INTO oh_authority_dependencies(space_id, record_key, dependency_key)
|
|
1207
|
+
SELECT ?, ?, ? WHERE ${existsOperation}`,
|
|
1208
|
+
args: [this.binding.spaceId, change.record.key, dependency, operation.operationSha256]
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
statements.push({
|
|
1213
|
+
sql: `UPDATE oh_authority_spaces SET generation = ?, head_operation_sha256 = ?,
|
|
1214
|
+
graph_revision_sha256 = ?, records_sha256 = ?, sequence = ?, updated_at = ?
|
|
1215
|
+
WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ? AND ${existsOperation}`,
|
|
1216
|
+
args: [operation.sequence, operation.operationSha256, operation.graphRevisionSha256,
|
|
1217
|
+
operation.recordsSha256, operation.sequence, operation.instant, this.binding.spaceId,
|
|
1218
|
+
current.generation, current.operationSha256, operation.operationSha256]
|
|
1219
|
+
});
|
|
1220
|
+
statements.push({
|
|
1221
|
+
sql: `INSERT INTO oh_authority_commit_guards(value)
|
|
1222
|
+
SELECT 'invalid' WHERE NOT EXISTS (SELECT 1 FROM oh_authority_spaces
|
|
1223
|
+
WHERE space_id = ? AND generation = ? AND head_operation_sha256 = ?)`,
|
|
1224
|
+
args: [this.binding.spaceId, operation.sequence, operation.operationSha256]
|
|
1225
|
+
});
|
|
1226
|
+
statements.push({
|
|
1227
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
1228
|
+
WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId]
|
|
1229
|
+
});
|
|
1230
|
+
statements.push({
|
|
1231
|
+
sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
1232
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId]
|
|
1233
|
+
});
|
|
1234
|
+
const write = yield* Effect.either(this.#client.batch(statements, "write"));
|
|
1235
|
+
if (Either.isLeft(write)) {
|
|
1236
|
+
const error = write.left;
|
|
1237
|
+
const raced = yield* this.#operationById(operationId);
|
|
1238
|
+
const head = yield* this.head();
|
|
1239
|
+
if (raced !== null && raced.actorId === actorId
|
|
1240
|
+
&& (yield* libSqlValue(() => canonicalJson(raced.changes))) === (yield* libSqlValue(() => canonicalJson(changes)))) {
|
|
1241
|
+
yield* this.#assertOperationReachable(raced, head);
|
|
1242
|
+
return raced;
|
|
1243
|
+
}
|
|
1244
|
+
if (head.operationSha256 !== current.operationSha256) {
|
|
1245
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The remote space head changed while committing.")));
|
|
1246
|
+
}
|
|
1247
|
+
return yield* Effect.fail(error);
|
|
1248
|
+
}
|
|
1249
|
+
const writeResults = write.right;
|
|
1250
|
+
if (writeResults.length !== statements.length) {
|
|
1251
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete commit result batch.")));
|
|
1252
|
+
}
|
|
1253
|
+
const persistedRow = writeResults.at(-2)?.rows[0];
|
|
1254
|
+
const persistedHeadRow = writeResults.at(-1)?.rows[0];
|
|
1255
|
+
if (persistedRow === undefined || persistedHeadRow === undefined) {
|
|
1256
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority omitted its persisted commit result.")));
|
|
1257
|
+
}
|
|
1258
|
+
const persisted = (yield* libSqlValue(() => parseOperationRow(persistedRow, { operationId, spaceId: this.binding.spaceId })));
|
|
1259
|
+
const persistedHead = (yield* libSqlValue(() => parseHeadRow(persistedHeadRow)));
|
|
1260
|
+
if ((yield* libSqlValue(() => canonicalJson(persisted))) !== (yield* libSqlValue(() => canonicalJson(operation)))
|
|
1261
|
+
|| persistedHead.operationSha256 !== operation.operationSha256
|
|
1262
|
+
|| persistedHead.sequence !== operation.sequence
|
|
1263
|
+
|| persistedHead.graphRevisionSha256 !== operation.graphRevisionSha256
|
|
1264
|
+
|| persistedHead.recordsSha256 !== operation.recordsSha256) {
|
|
1265
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority did not persist the committed operation exactly.")));
|
|
1266
|
+
}
|
|
1267
|
+
return persisted;
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
exportDependencyClosure(input: Readonly<{
|
|
1272
|
+
head?: OhHeadRefV1;
|
|
1273
|
+
maximumRecords?: number;
|
|
1274
|
+
roots: readonly string[];
|
|
1275
|
+
}>): Effect.Effect<OhDependencyClosureV1, LibSqlFailure> {
|
|
1276
|
+
return Effect.gen(this, function* () {
|
|
1277
|
+
if (!this.binding.profile.capabilities.dependencyClosureExport) {
|
|
1278
|
+
return yield* Effect.fail(libSqlFailure(new OhProfileError("This remote profile does not permit dependency-closure export.")));
|
|
1279
|
+
}
|
|
1280
|
+
const snapshot = yield* this.snapshot({
|
|
1281
|
+
...(input.head === undefined ? {} : { head: input.head }),
|
|
1282
|
+
...(input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords })
|
|
1283
|
+
});
|
|
1284
|
+
return (yield* libSqlValue(() => createOhDependencyClosureV1({
|
|
1285
|
+
binding: this.binding,
|
|
1286
|
+
...(input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }),
|
|
1287
|
+
roots: input.roots, snapshot
|
|
1288
|
+
})));
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
verify(): Effect.Effect<OhStoreVerificationV1, LibSqlFailure> {
|
|
1293
|
+
return Effect.gen(this, function* () {
|
|
1294
|
+
yield* libSqlValue(() => this.#assertOpen());
|
|
1295
|
+
const snapshot = yield* this.snapshot();
|
|
1296
|
+
yield* this.#assertMaterializedSnapshot(snapshot);
|
|
1297
|
+
return {
|
|
1298
|
+
head: snapshot.head, integrity: "verified", operations: snapshot.head.sequence,
|
|
1299
|
+
records: snapshot.records.length, v: 1
|
|
1300
|
+
};
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
#assertPurgeComplete(expected: OhSpacePurgeReceiptV1): Effect.Effect<void, LibSqlFailure> {
|
|
1305
|
+
return Effect.gen(this, function* () {
|
|
1306
|
+
yield* assertRemotePurgeComplete(this.#client, this.binding, expected);
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
purgeWorkingSpace(purgedAt: string): Effect.Effect<OhSpacePurgeReceiptV1, LibSqlFailure> {
|
|
1311
|
+
return Effect.gen(this, function* () {
|
|
1312
|
+
yield* libSqlValue(() => this.#assertOpen());
|
|
1313
|
+
if (this.binding.profile.profileKind !== "working"
|
|
1314
|
+
|| !this.binding.profile.capabilities.wholeSpacePurge) {
|
|
1315
|
+
return yield* Effect.fail(libSqlFailure(new OhProfileError("Whole-space purge requires a bound working profile.")));
|
|
1316
|
+
}
|
|
1317
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1318
|
+
const existing = yield* this.#readPurge();
|
|
1319
|
+
if (existing !== null) {
|
|
1320
|
+
yield* this.#assertPurgeComplete(existing);
|
|
1321
|
+
this.#purged = existing;
|
|
1322
|
+
return existing;
|
|
1323
|
+
}
|
|
1324
|
+
const head = yield* this.head();
|
|
1325
|
+
const receipt = (yield* libSqlValue(() => createOhSpacePurgeReceiptV1({ binding: this.binding, priorHead: head, purgedAt })));
|
|
1326
|
+
const receiptExists = "EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ? AND receipt_sha256 = ?)";
|
|
1327
|
+
const statements: OhLibSqlStatementV1[] = [{
|
|
1328
|
+
sql: `INSERT INTO oh_authority_purges(space_id,
|
|
1329
|
+
binding_sha256, prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json)
|
|
1330
|
+
SELECT ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (SELECT 1 FROM oh_authority_spaces
|
|
1331
|
+
WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?)
|
|
1332
|
+
AND EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ? AND binding_sha256 = ?)`,
|
|
1333
|
+
args: [this.binding.spaceId, this.binding.bindingSha256, head.operationSha256, head.sequence,
|
|
1334
|
+
receipt.purgedAt, receipt.receiptSha256, (yield* libSqlValue(() => canonicalJson(receipt))), this.binding.spaceId,
|
|
1335
|
+
head.generation, head.operationSha256, this.binding.spaceId, this.binding.bindingSha256]
|
|
1336
|
+
}];
|
|
1337
|
+
const guardedDelete = (table: string): OhLibSqlStatementV1 => ({
|
|
1338
|
+
sql: `DELETE FROM ${table} WHERE space_id = ? AND ${receiptExists}`,
|
|
1339
|
+
args: [this.binding.spaceId, this.binding.spaceId, receipt.receiptSha256],
|
|
1340
|
+
});
|
|
1341
|
+
statements.push({
|
|
1342
|
+
sql: `DELETE FROM oh_authority_operation_records
|
|
1343
|
+
WHERE operation_sha256 IN (SELECT operation_sha256 FROM oh_authority_operations WHERE space_id = ?)
|
|
1344
|
+
AND ${receiptExists}`,
|
|
1345
|
+
args: [this.binding.spaceId, this.binding.spaceId, receipt.receiptSha256]
|
|
1346
|
+
});
|
|
1347
|
+
statements.push(guardedDelete("oh_authority_dependencies"));
|
|
1348
|
+
statements.push(guardedDelete("oh_authority_records"));
|
|
1349
|
+
statements.push(guardedDelete("oh_authority_operations"));
|
|
1350
|
+
statements.push(guardedDelete("oh_authority_bindings"));
|
|
1351
|
+
statements.push(guardedDelete("oh_authority_spaces"));
|
|
1352
|
+
statements.push({
|
|
1353
|
+
sql: `INSERT INTO oh_authority_commit_guards(value)
|
|
1354
|
+
SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
1355
|
+
OR EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?)
|
|
1356
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operations WHERE space_id = ?)
|
|
1357
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operation_records WHERE space_id = ?)
|
|
1358
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operation_records AS materialized
|
|
1359
|
+
LEFT JOIN oh_authority_operations AS operation
|
|
1360
|
+
ON operation.operation_sha256 = materialized.operation_sha256
|
|
1361
|
+
WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id)
|
|
1362
|
+
OR EXISTS (SELECT 1 FROM oh_authority_records WHERE space_id = ?)
|
|
1363
|
+
OR EXISTS (SELECT 1 FROM oh_authority_dependencies WHERE space_id = ?)
|
|
1364
|
+
OR NOT ${receiptExists}`,
|
|
1365
|
+
args: [this.binding.spaceId, this.binding.spaceId, this.binding.spaceId,
|
|
1366
|
+
this.binding.spaceId, this.binding.spaceId, this.binding.spaceId,
|
|
1367
|
+
this.binding.spaceId, receipt.receiptSha256]
|
|
1368
|
+
});
|
|
1369
|
+
const write = yield* Effect.either(this.#client.batch(statements, "write"));
|
|
1370
|
+
if (Either.isLeft(write)) {
|
|
1371
|
+
const raced = yield* this.#readPurge();
|
|
1372
|
+
if (raced !== null) {
|
|
1373
|
+
yield* this.#assertPurgeComplete(raced);
|
|
1374
|
+
this.#purged = raced;
|
|
1375
|
+
return raced;
|
|
1376
|
+
}
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
const persisted = yield* this.#readPurge();
|
|
1380
|
+
if (persisted !== null) {
|
|
1381
|
+
yield* this.#assertPurgeComplete(persisted);
|
|
1382
|
+
this.#purged = persisted;
|
|
1383
|
+
return persisted;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The remote working space changed repeatedly while purging.")));
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/** Opens a direct libSQL/Turso authority; this is not operation-log sync. */
|
|
1393
|
+
export function createAuthority(
|
|
1394
|
+
client: LibSqlAuthorityClientService,
|
|
1395
|
+
options: OhLibSqlStoreAuthorityOptionsV1 = {},
|
|
1396
|
+
): Effect.Effect<LibSqlStoreProgram, LibSqlFailure> {
|
|
1397
|
+
return Effect.gen(function* () {
|
|
1398
|
+
const profile = (yield* libSqlValue(() => parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1)));
|
|
1399
|
+
if (profile === null) return yield* Effect.fail(libSqlFailure(new TypeError("Invalid libSQL store profile.")));
|
|
1400
|
+
const spaceId = options.spaceId ?? "default";
|
|
1401
|
+
const binding = (yield* libSqlValue(() => createOhStoreBindingV1({
|
|
1402
|
+
profile,
|
|
1403
|
+
realmId: options.realmId ?? `realm:${spaceId}`, spaceId, v: 1
|
|
1404
|
+
})));
|
|
1405
|
+
yield* verifyAuthoritySchema(client);
|
|
1406
|
+
yield* initializeSpace(client, binding);
|
|
1407
|
+
return new LibSqlStoreProgram(client, binding);
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Opens an already-bound direct libSQL/Turso authority without creating or
|
|
1413
|
+
* updating data. This seam is for separately held read or purge custody that
|
|
1414
|
+
* must fail closed instead of acquiring space-creation authority.
|
|
1415
|
+
*/
|
|
1416
|
+
export function openExistingAuthority(
|
|
1417
|
+
client: LibSqlAuthorityClientService,
|
|
1418
|
+
options: OhLibSqlStoreAuthorityOptionsV1 = {},
|
|
1419
|
+
): Effect.Effect<LibSqlStoreProgram, LibSqlFailure> {
|
|
1420
|
+
return Effect.gen(function* () {
|
|
1421
|
+
const profile = (yield* libSqlValue(() => parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1)));
|
|
1422
|
+
if (profile === null) return yield* Effect.fail(libSqlFailure(new TypeError("Invalid libSQL store profile.")));
|
|
1423
|
+
const spaceId = options.spaceId ?? "default";
|
|
1424
|
+
const binding = (yield* libSqlValue(() => createOhStoreBindingV1({
|
|
1425
|
+
profile,
|
|
1426
|
+
realmId: options.realmId ?? `realm:${spaceId}`, spaceId, v: 1
|
|
1427
|
+
})));
|
|
1428
|
+
yield* verifyAuthoritySchema(client);
|
|
1429
|
+
yield* requireExistingSpace(client, binding);
|
|
1430
|
+
return new LibSqlStoreProgram(client, binding);
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Purges an existing working authority or atomically fences its exact binding
|
|
1436
|
+
* when creation never completed. The empty-space receipt prevents a delayed
|
|
1437
|
+
* creator from resurrecting abandoned custody without granting the purge
|
|
1438
|
+
* credential permission to create a space or binding.
|
|
1439
|
+
*/
|
|
1440
|
+
export function purgeWorkingSpace(
|
|
1441
|
+
client: LibSqlAuthorityClientService,
|
|
1442
|
+
options: OhLibSqlStoreAuthorityOptionsV1 & Readonly<{ purgedAt?: string }> = {},
|
|
1443
|
+
): Effect.Effect<OhSpacePurgeReceiptV1, LibSqlFailure> {
|
|
1444
|
+
return Effect.gen(function* () {
|
|
1445
|
+
|
|
1446
|
+
const profile = (yield* libSqlValue(() => parseOhStoreProfileV1(options.profile ?? OH_WORKING_STORE_PROFILE_V1)));
|
|
1447
|
+
if (profile === null) return yield* Effect.fail(libSqlFailure(new TypeError("Invalid libSQL store profile.")));
|
|
1448
|
+
if (profile.profileKind !== "working" || !profile.capabilities.wholeSpacePurge) {
|
|
1449
|
+
return yield* Effect.fail(libSqlFailure(new OhProfileError("Whole-space purge requires a bound working profile.")));
|
|
1450
|
+
}
|
|
1451
|
+
const spaceId = options.spaceId ?? "default";
|
|
1452
|
+
const binding = (yield* libSqlValue(() => createOhStoreBindingV1({
|
|
1453
|
+
profile,
|
|
1454
|
+
realmId: options.realmId ?? `realm:${spaceId}`, spaceId, v: 1
|
|
1455
|
+
})));
|
|
1456
|
+
const purgedAt = options.purgedAt ?? (yield* client.currentInstant);
|
|
1457
|
+
yield* verifyAuthoritySchema(client);
|
|
1458
|
+
|
|
1459
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1460
|
+
const proof = yield* client.batch([
|
|
1461
|
+
{ sql: BINDING_ROW_SELECT, args: [binding.spaceId] },
|
|
1462
|
+
{ sql: SPACE_PURGE_PROOF_SELECT, args: [binding.spaceId] },
|
|
1463
|
+
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] },
|
|
1464
|
+
], "read");
|
|
1465
|
+
if (proof.length !== 3) {
|
|
1466
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete purge proof.")));
|
|
1467
|
+
}
|
|
1468
|
+
const [bindingResult, spaceResult, purgeResult] = proof as
|
|
1469
|
+
[OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1];
|
|
1470
|
+
const existingPurge = purgeResult.rows[0];
|
|
1471
|
+
if (existingPurge !== undefined) {
|
|
1472
|
+
const receipt = (yield* libSqlValue(() => parsePurgeReceiptRow(existingPurge, binding.spaceId, binding.bindingSha256)));
|
|
1473
|
+
yield* assertRemotePurgeComplete(client, binding, receipt);
|
|
1474
|
+
return receipt;
|
|
1475
|
+
}
|
|
1476
|
+
const bindingRow = bindingResult.rows[0];
|
|
1477
|
+
const spaceRow = spaceResult.rows[0];
|
|
1478
|
+
if ((bindingRow === undefined) !== (spaceRow === undefined)) {
|
|
1479
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority has only half of its space binding.")));
|
|
1480
|
+
}
|
|
1481
|
+
if (bindingRow !== undefined && spaceRow !== undefined) {
|
|
1482
|
+
const persisted = (yield* libSqlValue(() => parseBindingRow(bindingRow, binding.spaceId)));
|
|
1483
|
+
if ((yield* libSqlValue(() => canonicalJson(persisted))) !== (yield* libSqlValue(() => canonicalJson(binding)))) {
|
|
1484
|
+
return yield* Effect.fail(libSqlFailure(new OhProfileError("The remote space is bound to a different realm or profile.")));
|
|
1485
|
+
}
|
|
1486
|
+
if (rowValue(spaceRow, "contract_id", 5) !== OH_CONTRACT_MANIFEST_V1.contractId) {
|
|
1487
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The existing remote space uses a different Oh contract.")));
|
|
1488
|
+
}
|
|
1489
|
+
(yield* libSqlValue(() => parseHeadRow(spaceRow)));
|
|
1490
|
+
const authority = new LibSqlStoreProgram(client, binding);
|
|
1491
|
+
return yield* authority.purgeWorkingSpace(purgedAt);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const receipt = (yield* libSqlValue(() => createOhSpacePurgeReceiptV1({
|
|
1495
|
+
binding,
|
|
1496
|
+
priorHead: emptyOhHeadV1(),
|
|
1497
|
+
purgedAt,
|
|
1498
|
+
})));
|
|
1499
|
+
const receiptJson = (yield* libSqlValue(() => canonicalJson(receipt)));
|
|
1500
|
+
const write = yield* Effect.either(client.batch([
|
|
1501
|
+
{
|
|
1502
|
+
sql: `INSERT INTO oh_authority_purges(space_id, binding_sha256,
|
|
1503
|
+
prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json)
|
|
1504
|
+
SELECT ?, ?, NULL, 0, ?, ?, ?
|
|
1505
|
+
WHERE NOT EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
1506
|
+
AND NOT EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?)
|
|
1507
|
+
ON CONFLICT(space_id) DO NOTHING`,
|
|
1508
|
+
args: [binding.spaceId, binding.bindingSha256, receipt.purgedAt,
|
|
1509
|
+
receipt.receiptSha256, receiptJson, binding.spaceId, binding.spaceId]
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
sql: `INSERT INTO oh_authority_commit_guards(value)
|
|
1513
|
+
SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
1514
|
+
OR EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?)
|
|
1515
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operations WHERE space_id = ?)
|
|
1516
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operation_records WHERE space_id = ?)
|
|
1517
|
+
OR EXISTS (SELECT 1 FROM oh_authority_records WHERE space_id = ?)
|
|
1518
|
+
OR EXISTS (SELECT 1 FROM oh_authority_dependencies WHERE space_id = ?)
|
|
1519
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operation_records AS materialized
|
|
1520
|
+
LEFT JOIN oh_authority_operations AS operation
|
|
1521
|
+
ON operation.operation_sha256 = materialized.operation_sha256
|
|
1522
|
+
WHERE operation.operation_sha256 IS NULL
|
|
1523
|
+
OR operation.space_id <> materialized.space_id)
|
|
1524
|
+
OR NOT EXISTS (SELECT 1 FROM oh_authority_purges
|
|
1525
|
+
WHERE space_id = ? AND receipt_sha256 = ?)`,
|
|
1526
|
+
args: [binding.spaceId, binding.spaceId, binding.spaceId, binding.spaceId,
|
|
1527
|
+
binding.spaceId, binding.spaceId, binding.spaceId, receipt.receiptSha256]
|
|
1528
|
+
},
|
|
1529
|
+
], "write"));
|
|
1530
|
+
if (Either.isLeft(write)) {
|
|
1531
|
+
const error = write.left;
|
|
1532
|
+
const recovery = yield* client.batch([
|
|
1533
|
+
{ sql: BINDING_ROW_SELECT, args: [binding.spaceId] },
|
|
1534
|
+
{ sql: SPACE_PURGE_PROOF_SELECT, args: [binding.spaceId] },
|
|
1535
|
+
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] },
|
|
1536
|
+
], "read");
|
|
1537
|
+
if (recovery.length !== 3) {
|
|
1538
|
+
return yield* Effect.fail(libSqlFailure(new OhIntegrityError("The remote authority returned an incomplete purge recovery proof.")));
|
|
1539
|
+
}
|
|
1540
|
+
const raced = recovery[2]?.rows[0];
|
|
1541
|
+
if (raced !== undefined) {
|
|
1542
|
+
const persisted = (yield* libSqlValue(() => parsePurgeReceiptRow(raced, binding.spaceId, binding.bindingSha256)));
|
|
1543
|
+
yield* assertRemotePurgeComplete(client, binding, persisted);
|
|
1544
|
+
return persisted;
|
|
1545
|
+
}
|
|
1546
|
+
if (recovery[0]?.rows[0] !== undefined || recovery[1]?.rows[0] !== undefined) continue;
|
|
1547
|
+
return yield* Effect.fail(error);
|
|
1548
|
+
}
|
|
1549
|
+
const persisted = yield* queryOne(client, { sql: PURGE_ROW_SELECT, args: [binding.spaceId] });
|
|
1550
|
+
if (persisted === null) continue;
|
|
1551
|
+
const exact = (yield* libSqlValue(() => parsePurgeReceiptRow(persisted, binding.spaceId, binding.bindingSha256)));
|
|
1552
|
+
yield* assertRemotePurgeComplete(client, binding, exact);
|
|
1553
|
+
return exact;
|
|
1554
|
+
}
|
|
1555
|
+
return yield* Effect.fail(libSqlFailure(new OhConflictError("The remote working space changed repeatedly while fencing purge.")));
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
/** Admission and drain are one local authority lifetime. SQL still arbitrates
|
|
1559
|
+
* concurrent commits; this gate does not serialize independent operations. */
|
|
1560
|
+
export const makeLibSqlOwner = Effect.gen(function* () {
|
|
1561
|
+
const active = yield* Ref.make(0);
|
|
1562
|
+
const closing = yield* Ref.make(false);
|
|
1563
|
+
const drained = yield* Deferred.make<void>();
|
|
1564
|
+
const admit = Effect.gen(function* () {
|
|
1565
|
+
if (yield* Ref.get(closing)) return false;
|
|
1566
|
+
yield* Ref.update(active, count => count + 1);
|
|
1567
|
+
return true;
|
|
1568
|
+
});
|
|
1569
|
+
const release = Effect.gen(function* () {
|
|
1570
|
+
const remaining = yield* Ref.updateAndGet(active, count => count - 1);
|
|
1571
|
+
if (remaining === 0 && (yield* Ref.get(closing))) yield* Deferred.succeed(drained, undefined);
|
|
1572
|
+
});
|
|
1573
|
+
const beginClose = Effect.gen(function* () {
|
|
1574
|
+
yield* Ref.set(closing, true);
|
|
1575
|
+
if ((yield* Ref.get(active)) === 0) yield* Deferred.succeed(drained, undefined);
|
|
1576
|
+
});
|
|
1577
|
+
return {
|
|
1578
|
+
admit, beginClose,
|
|
1579
|
+
drained: Deferred.await(drained),
|
|
1580
|
+
complete: <A>(operation: Effect.Effect<A, LibSqlFailure>): Effect.Effect<A, LibSqlFailure> =>
|
|
1581
|
+
Effect.uninterruptible(operation.pipe(Effect.ensuring(release))),
|
|
1582
|
+
};
|
|
1583
|
+
});
|