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