@hraness/oh 0.2.3
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 +21 -0
- package/README.md +598 -0
- package/dist/canonical.d.ts +32 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3419 -0
- package/dist/contract.d.ts +33 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/graph.d.ts +67 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1988 -0
- package/dist/libsql.d.ts +57 -0
- package/dist/libsql.d.ts.map +1 -0
- package/dist/libsql.js +2662 -0
- package/dist/memory.d.ts +366 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +3650 -0
- package/dist/ontology.d.ts +242 -0
- package/dist/ontology.d.ts.map +1 -0
- package/dist/operation.d.ts +24 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/projection-public.d.ts +59 -0
- package/dist/projection-public.d.ts.map +1 -0
- package/dist/projection-public.js +1682 -0
- package/dist/projection-suss.d.ts +17 -0
- package/dist/projection-suss.d.ts.map +1 -0
- package/dist/projection-suss.js +1721 -0
- package/dist/projection.d.ts +315 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/schema.d.ts +45 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/sdk.d.ts +51 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +3072 -0
- package/dist/search.d.ts +34 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/semantic.d.ts +83 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +706 -0
- package/dist/sqlite/driver.d.ts +6 -0
- package/dist/sqlite/driver.d.ts.map +1 -0
- package/dist/sqlite/index.d.ts +5 -0
- package/dist/sqlite/index.d.ts.map +1 -0
- package/dist/sqlite/index.js +2840 -0
- package/dist/sqlite/migrations.d.ts +10 -0
- package/dist/sqlite/migrations.d.ts.map +1 -0
- package/dist/sqlite/port.d.ts +40 -0
- package/dist/sqlite/port.d.ts.map +1 -0
- package/dist/sqlite/runtime.d.ts +23 -0
- package/dist/sqlite/runtime.d.ts.map +1 -0
- package/dist/sqlite/store.d.ts +85 -0
- package/dist/sqlite/store.d.ts.map +1 -0
- package/dist/store.d.ts +236 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +996 -0
- package/dist/sync.d.ts +57 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +1328 -0
- package/package.json +121 -0
- package/skills/oh/SKILL.md +206 -0
- package/skills/oh/agents/openai.yaml +4 -0
- package/spec/README.md +74 -0
- package/spec/manifest.json +41 -0
- package/spec/v1/canonical-json.md +59 -0
- package/spec/v1/contract.json +28 -0
- package/spec/v1/contract.schema.json +58 -0
- package/spec/v1/embedding-profile.json +11 -0
- package/spec/v1/embedding.md +56 -0
- package/spec/v1/graph.md +87 -0
- package/spec/v1/memory.md +193 -0
- package/spec/v1/migration.md +92 -0
- package/spec/v1/ontology.json +55 -0
- package/spec/v1/ontology.md +80 -0
- package/spec/v1/operation.schema.json +138 -0
- package/spec/v1/projection-identity.schema.json +58 -0
- package/spec/v1/projection-query.schema.json +60 -0
- package/spec/v1/projection-result.schema.json +452 -0
- package/spec/v1/projection-rule-pack.schema.json +182 -0
- package/spec/v1/projection.md +165 -0
- package/spec/v1/record.schema.json +95 -0
- package/spec/v1/schema-evolution.md +51 -0
- package/spec/v1/schema-revision.schema.json +178 -0
- package/spec/v1/storage.md +88 -0
- package/spec/v1/store.md +131 -0
- package/spec/v1/sync-bundle.schema.json +51 -0
- package/spec/v1/sync.md +67 -0
- package/src/canonical.test.ts +46 -0
- package/src/canonical.ts +203 -0
- package/src/cli.test.ts +103 -0
- package/src/cli.ts +308 -0
- package/src/contract.ts +87 -0
- package/src/contracts.test.ts +147 -0
- package/src/graph.ts +248 -0
- package/src/index.ts +8 -0
- package/src/libsql.test.ts +657 -0
- package/src/libsql.ts +1687 -0
- package/src/memory.test.ts +783 -0
- package/src/memory.ts +1684 -0
- package/src/ontology.ts +573 -0
- package/src/operation.ts +80 -0
- package/src/projection-public.ts +53 -0
- package/src/projection-suss.ts +129 -0
- package/src/projection.test.ts +418 -0
- package/src/projection.ts +1457 -0
- package/src/schema.ts +156 -0
- package/src/sdk.ts +96 -0
- package/src/search.ts +66 -0
- package/src/semantic.test.ts +480 -0
- package/src/semantic.ts +333 -0
- package/src/sqlite/driver.ts +47 -0
- package/src/sqlite/index.ts +4 -0
- package/src/sqlite/migrations.test.ts +44 -0
- package/src/sqlite/migrations.ts +178 -0
- package/src/sqlite/port.test.ts +127 -0
- package/src/sqlite/port.ts +120 -0
- package/src/sqlite/runtime.test.ts +68 -0
- package/src/sqlite/runtime.ts +53 -0
- package/src/sqlite/store.test.ts +295 -0
- package/src/sqlite/store.ts +988 -0
- package/src/store.test.ts +121 -0
- package/src/store.ts +701 -0
- package/src/sync.test.ts +117 -0
- package/src/sync.ts +227 -0
package/dist/libsql.js
ADDED
|
@@ -0,0 +1,2662 @@
|
|
|
1
|
+
// src/canonical.ts
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
class OhValidationError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
path;
|
|
7
|
+
constructor(code, path, message) {
|
|
8
|
+
super(`${path}: ${message}`);
|
|
9
|
+
this.name = "OhValidationError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.path = path;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isPlainRecord(value) {
|
|
15
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
16
|
+
return false;
|
|
17
|
+
const prototype = Object.getPrototypeOf(value);
|
|
18
|
+
return prototype === Object.prototype || prototype === null;
|
|
19
|
+
}
|
|
20
|
+
function hasExactKeys(value, keys) {
|
|
21
|
+
const actual = Object.keys(value);
|
|
22
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
23
|
+
}
|
|
24
|
+
function assertUnicodeScalarString(value, path) {
|
|
25
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
26
|
+
const code = value.charCodeAt(index);
|
|
27
|
+
if (code >= 55296 && code <= 56319) {
|
|
28
|
+
const next = value.charCodeAt(index + 1);
|
|
29
|
+
if (!(next >= 56320 && next <= 57343)) {
|
|
30
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
31
|
+
}
|
|
32
|
+
index += 1;
|
|
33
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
34
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function encodeCanonical(value, path, ancestors) {
|
|
39
|
+
if (value === null || typeof value === "boolean")
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
assertUnicodeScalarString(value, path);
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "number") {
|
|
46
|
+
if (!Number.isFinite(value)) {
|
|
47
|
+
throw new OhValidationError("non-json-number", path, "must be finite");
|
|
48
|
+
}
|
|
49
|
+
if (Object.is(value, -0)) {
|
|
50
|
+
throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical");
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
if (typeof value !== "object" || value === null) {
|
|
55
|
+
throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`);
|
|
56
|
+
}
|
|
57
|
+
if (ancestors.has(value)) {
|
|
58
|
+
throw new OhValidationError("cycle", path, "contains a cycle");
|
|
59
|
+
}
|
|
60
|
+
ancestors.add(value);
|
|
61
|
+
try {
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
const encoded = [];
|
|
64
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
65
|
+
if (!Object.hasOwn(value, index)) {
|
|
66
|
+
throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
|
|
67
|
+
}
|
|
68
|
+
encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors));
|
|
69
|
+
}
|
|
70
|
+
const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length));
|
|
71
|
+
if (extraKeys.length > 0) {
|
|
72
|
+
throw new OhValidationError("non-json-property", path, "array has non-index properties");
|
|
73
|
+
}
|
|
74
|
+
return `[${encoded.join(",")}]`;
|
|
75
|
+
}
|
|
76
|
+
if (!isPlainRecord(value)) {
|
|
77
|
+
throw new OhValidationError("non-plain-object", path, "must be a plain object");
|
|
78
|
+
}
|
|
79
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
80
|
+
if (ownKeys.some((key) => typeof key !== "string")) {
|
|
81
|
+
throw new OhValidationError("non-json-property", path, "object has a symbol property");
|
|
82
|
+
}
|
|
83
|
+
const keys = ownKeys;
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
86
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
87
|
+
throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
keys.sort();
|
|
91
|
+
const entries = keys.map((key) => {
|
|
92
|
+
assertUnicodeScalarString(key, `${path}.<key>`);
|
|
93
|
+
return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`;
|
|
94
|
+
});
|
|
95
|
+
return `{${entries.join(",")}}`;
|
|
96
|
+
} finally {
|
|
97
|
+
ancestors.delete(value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function canonicalJson(value) {
|
|
101
|
+
return encodeCanonical(value, "$", new Set);
|
|
102
|
+
}
|
|
103
|
+
function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) {
|
|
104
|
+
if (utf8ByteLength(text) > maximumBytes) {
|
|
105
|
+
throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit");
|
|
106
|
+
}
|
|
107
|
+
let value;
|
|
108
|
+
try {
|
|
109
|
+
value = JSON.parse(text);
|
|
110
|
+
} catch {
|
|
111
|
+
throw new OhValidationError("invalid-json", "$", "is not valid JSON");
|
|
112
|
+
}
|
|
113
|
+
if (canonicalJson(value) !== text) {
|
|
114
|
+
throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical");
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function utf8ByteLength(value) {
|
|
119
|
+
return Buffer.byteLength(value, "utf8");
|
|
120
|
+
}
|
|
121
|
+
function sha256Hex(value) {
|
|
122
|
+
return createHash("sha256").update(value).digest("hex");
|
|
123
|
+
}
|
|
124
|
+
function canonicalSha256(value) {
|
|
125
|
+
return sha256Hex(canonicalJson(value));
|
|
126
|
+
}
|
|
127
|
+
function parseSha256Hex(value) {
|
|
128
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null;
|
|
129
|
+
}
|
|
130
|
+
function parseCanonicalInstantV1(value) {
|
|
131
|
+
if (typeof value !== "string" || !/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/u.test(value)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const timestamp = Date.parse(value);
|
|
135
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null;
|
|
136
|
+
}
|
|
137
|
+
function canonicalNow() {
|
|
138
|
+
return new Date().toISOString();
|
|
139
|
+
}
|
|
140
|
+
function safeCode(value, maximumLength = 128) {
|
|
141
|
+
return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
142
|
+
}
|
|
143
|
+
function orderedUnique(values, key) {
|
|
144
|
+
return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value));
|
|
145
|
+
}
|
|
146
|
+
function sortUnique(values, key) {
|
|
147
|
+
const sorted = [...values].sort((left, right) => {
|
|
148
|
+
const leftKey = key(left);
|
|
149
|
+
const rightKey = key(right);
|
|
150
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
151
|
+
});
|
|
152
|
+
if (!orderedUnique(sorted, key)) {
|
|
153
|
+
throw new OhValidationError("duplicate", "$", "contains duplicate canonical values");
|
|
154
|
+
}
|
|
155
|
+
return sorted;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/graph.ts
|
|
159
|
+
var OH_GRAPH_FORMAT_VERSION_V1 = 1;
|
|
160
|
+
var OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
161
|
+
changesPerOperation: 8192,
|
|
162
|
+
dependenciesPerRecord: 4096,
|
|
163
|
+
recordBytes: 1024 * 1024,
|
|
164
|
+
recordsPerSnapshot: 65536
|
|
165
|
+
});
|
|
166
|
+
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
167
|
+
"activity",
|
|
168
|
+
"assertion",
|
|
169
|
+
"context",
|
|
170
|
+
"dependency-manifest",
|
|
171
|
+
"edition",
|
|
172
|
+
"entity",
|
|
173
|
+
"evidence",
|
|
174
|
+
"identity-operation",
|
|
175
|
+
"inquiry",
|
|
176
|
+
"inquiry-event",
|
|
177
|
+
"review-decision",
|
|
178
|
+
"rights-decision",
|
|
179
|
+
"schema",
|
|
180
|
+
"shape",
|
|
181
|
+
"statement",
|
|
182
|
+
"type-membership",
|
|
183
|
+
"view",
|
|
184
|
+
"vocabulary"
|
|
185
|
+
];
|
|
186
|
+
function recordKey(value) {
|
|
187
|
+
return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
188
|
+
}
|
|
189
|
+
function createKnowledgeGraphRecordV1(input) {
|
|
190
|
+
if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies))
|
|
191
|
+
throw new TypeError("Invalid graph record input.");
|
|
192
|
+
const key = recordKey(input.key);
|
|
193
|
+
const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind);
|
|
194
|
+
if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
|
|
195
|
+
throw new TypeError("Invalid graph record identity.");
|
|
196
|
+
const dependencies = input.dependencies.map(recordKey);
|
|
197
|
+
if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) {
|
|
198
|
+
throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive.");
|
|
199
|
+
}
|
|
200
|
+
const valueJson = canonicalJson(input.value);
|
|
201
|
+
if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) {
|
|
202
|
+
throw new RangeError("Graph record value exceeds its canonical byte limit.");
|
|
203
|
+
}
|
|
204
|
+
const payload = { dependencies, key, kind, v: 1, value: input.value };
|
|
205
|
+
return { ...payload, recordSha256: canonicalSha256(payload) };
|
|
206
|
+
}
|
|
207
|
+
function parseKnowledgeGraphRecordV1(value) {
|
|
208
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256"))
|
|
209
|
+
return null;
|
|
210
|
+
const recordSha256 = parseSha256Hex(value.recordSha256);
|
|
211
|
+
const { recordSha256: _digest, ...input } = value;
|
|
212
|
+
try {
|
|
213
|
+
const created = createKnowledgeGraphRecordV1(input);
|
|
214
|
+
return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null;
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function knowledgeGraphRecordRefV1(record) {
|
|
220
|
+
return {
|
|
221
|
+
dependencies: record.dependencies,
|
|
222
|
+
key: record.key,
|
|
223
|
+
kind: record.kind,
|
|
224
|
+
sha256: record.recordSha256,
|
|
225
|
+
v: 1
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function changeKey(change) {
|
|
229
|
+
return change.kind === "put" ? change.record.key : change.key;
|
|
230
|
+
}
|
|
231
|
+
function canonicalKnowledgeGraphChangesV1(changes) {
|
|
232
|
+
const normalized = [];
|
|
233
|
+
for (const change of changes) {
|
|
234
|
+
if (!isPlainRecord(change) || change.v !== 1)
|
|
235
|
+
throw new TypeError("Invalid graph change.");
|
|
236
|
+
if (change.kind === "put") {
|
|
237
|
+
const record = parseKnowledgeGraphRecordV1(change.record);
|
|
238
|
+
if (record === null)
|
|
239
|
+
throw new TypeError("Invalid graph record in change.");
|
|
240
|
+
normalized.push({ kind: "put", record, v: 1 });
|
|
241
|
+
} else if (change.kind === "tombstone") {
|
|
242
|
+
const key = recordKey(change.key);
|
|
243
|
+
const priorSha256 = parseSha256Hex(change.priorSha256);
|
|
244
|
+
if (key === null || priorSha256 === null)
|
|
245
|
+
throw new TypeError("Invalid graph tombstone.");
|
|
246
|
+
normalized.push({ key, kind: "tombstone", priorSha256, v: 1 });
|
|
247
|
+
} else
|
|
248
|
+
throw new TypeError("Unknown graph change kind.");
|
|
249
|
+
}
|
|
250
|
+
return sortUnique(normalized, changeKey);
|
|
251
|
+
}
|
|
252
|
+
function graphRevisionSha256V1(input) {
|
|
253
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
254
|
+
const operationId = safeCode(input.operationId);
|
|
255
|
+
const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256);
|
|
256
|
+
const recordsSha256 = parseSha256Hex(input.recordsSha256);
|
|
257
|
+
const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null;
|
|
258
|
+
if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation || operationId === null || recordsSha256 === null || revision === null || input.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || revision === 1 !== (parentGraphRevisionSha256 === null)) {
|
|
259
|
+
throw new TypeError("Invalid graph revision digest input.");
|
|
260
|
+
}
|
|
261
|
+
return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/ontology.ts
|
|
265
|
+
var OH_ONTOLOGY_VERSION_V1 = "1.0.0";
|
|
266
|
+
var OH_CONTRACT_ID_V1 = "oh.ontology.v1";
|
|
267
|
+
var OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({
|
|
268
|
+
dimensions: 64,
|
|
269
|
+
listValues: 256,
|
|
270
|
+
qualifiers: 128,
|
|
271
|
+
statementBytes: 256 * 1024,
|
|
272
|
+
textBytes: 64 * 1024
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// src/schema.ts
|
|
276
|
+
var OH_SCHEMA_FORMAT_VERSION_V1 = 1;
|
|
277
|
+
|
|
278
|
+
// src/contract.ts
|
|
279
|
+
var manifestPayload = Object.freeze({
|
|
280
|
+
contractId: OH_CONTRACT_ID_V1,
|
|
281
|
+
graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1,
|
|
282
|
+
ontologyVersion: OH_ONTOLOGY_VERSION_V1,
|
|
283
|
+
recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1,
|
|
284
|
+
schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1,
|
|
285
|
+
v: 1
|
|
286
|
+
});
|
|
287
|
+
var OH_CONTRACT_MANIFEST_V1 = Object.freeze({
|
|
288
|
+
...manifestPayload,
|
|
289
|
+
contractSha256: canonicalSha256(manifestPayload)
|
|
290
|
+
});
|
|
291
|
+
class OhRecordCodecRegistry {
|
|
292
|
+
#codecs = new Map;
|
|
293
|
+
#sealed = false;
|
|
294
|
+
register(codec) {
|
|
295
|
+
if (this.#sealed)
|
|
296
|
+
throw new TypeError("The codec registry is sealed.");
|
|
297
|
+
if (this.#codecs.has(codec.kind))
|
|
298
|
+
throw new TypeError(`A codec is already registered for ${codec.kind}.`);
|
|
299
|
+
this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse }));
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
parse(kind, value) {
|
|
303
|
+
const codec = this.#codecs.get(kind);
|
|
304
|
+
if (codec !== undefined)
|
|
305
|
+
return codec.parse(value);
|
|
306
|
+
try {
|
|
307
|
+
canonicalJson(value);
|
|
308
|
+
return value;
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
has(kind) {
|
|
314
|
+
return this.#codecs.has(kind);
|
|
315
|
+
}
|
|
316
|
+
parseRequired(kind, value) {
|
|
317
|
+
const codec = this.#codecs.get(kind);
|
|
318
|
+
if (codec === undefined)
|
|
319
|
+
return null;
|
|
320
|
+
try {
|
|
321
|
+
const parsed = codec.parse(value);
|
|
322
|
+
if (parsed === null)
|
|
323
|
+
return null;
|
|
324
|
+
canonicalJson(parsed);
|
|
325
|
+
return parsed;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
seal() {
|
|
331
|
+
this.#sealed = true;
|
|
332
|
+
return this;
|
|
333
|
+
}
|
|
334
|
+
get sealed() {
|
|
335
|
+
return this.#sealed;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// src/operation.ts
|
|
339
|
+
var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024;
|
|
340
|
+
function parsePayload(value) {
|
|
341
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
342
|
+
"actorId",
|
|
343
|
+
"changes",
|
|
344
|
+
"contractId",
|
|
345
|
+
"graphRevisionSha256",
|
|
346
|
+
"instant",
|
|
347
|
+
"operationId",
|
|
348
|
+
"parentOperationSha256",
|
|
349
|
+
"recordsSha256",
|
|
350
|
+
"sequence",
|
|
351
|
+
"spaceId",
|
|
352
|
+
"v"
|
|
353
|
+
]) || value.v !== 1 || value.contractId !== OH_CONTRACT_ID_V1 || !Array.isArray(value.changes))
|
|
354
|
+
return null;
|
|
355
|
+
const actorId = safeCode(value.actorId);
|
|
356
|
+
const operationId = safeCode(value.operationId);
|
|
357
|
+
const spaceId = safeCode(value.spaceId);
|
|
358
|
+
const graphRevisionSha256 = parseSha256Hex(value.graphRevisionSha256);
|
|
359
|
+
const parentOperationSha256 = value.parentOperationSha256 === null ? null : parseSha256Hex(value.parentOperationSha256);
|
|
360
|
+
const recordsSha256 = parseSha256Hex(value.recordsSha256);
|
|
361
|
+
const instant = parseCanonicalInstantV1(value.instant);
|
|
362
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence > 0 ? value.sequence : null;
|
|
363
|
+
let changes;
|
|
364
|
+
try {
|
|
365
|
+
changes = canonicalKnowledgeGraphChangesV1(value.changes);
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation)
|
|
370
|
+
return null;
|
|
371
|
+
return actorId !== null && operationId !== null && spaceId !== null && graphRevisionSha256 !== null && recordsSha256 !== null && instant !== null && sequence !== null && (value.parentOperationSha256 === null || parentOperationSha256 !== null) && sequence === 1 === (parentOperationSha256 === null) ? {
|
|
372
|
+
actorId,
|
|
373
|
+
changes,
|
|
374
|
+
contractId: OH_CONTRACT_ID_V1,
|
|
375
|
+
graphRevisionSha256,
|
|
376
|
+
instant,
|
|
377
|
+
operationId,
|
|
378
|
+
parentOperationSha256,
|
|
379
|
+
recordsSha256,
|
|
380
|
+
sequence,
|
|
381
|
+
spaceId,
|
|
382
|
+
v: 1
|
|
383
|
+
} : null;
|
|
384
|
+
}
|
|
385
|
+
function createOhOperationV1(input) {
|
|
386
|
+
const payload = parsePayload(input);
|
|
387
|
+
if (payload === null)
|
|
388
|
+
throw new TypeError("Invalid Oh operation payload.");
|
|
389
|
+
const operation = { ...payload, operationSha256: canonicalSha256(payload) };
|
|
390
|
+
if (Buffer.byteLength(canonicalJson(operation), "utf8") > OH_OPERATION_MAX_BYTES_V1) {
|
|
391
|
+
throw new RangeError("Oh operation exceeds its canonical byte limit.");
|
|
392
|
+
}
|
|
393
|
+
return operation;
|
|
394
|
+
}
|
|
395
|
+
function parseOhOperationV1(value) {
|
|
396
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "operationSha256"))
|
|
397
|
+
return null;
|
|
398
|
+
const operationSha256 = parseSha256Hex(value.operationSha256);
|
|
399
|
+
const { operationSha256: _digest, ...input } = value;
|
|
400
|
+
const payload = parsePayload(input);
|
|
401
|
+
return operationSha256 !== null && payload !== null && Buffer.byteLength(canonicalJson({ ...payload, operationSha256 }), "utf8") <= OH_OPERATION_MAX_BYTES_V1 && canonicalSha256(payload) === operationSha256 ? { ...payload, operationSha256 } : null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/store.ts
|
|
405
|
+
class OhConflictError extends Error {
|
|
406
|
+
constructor(message) {
|
|
407
|
+
super(message);
|
|
408
|
+
this.name = "OhConflictError";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
class OhIntegrityError extends Error {
|
|
413
|
+
constructor(message) {
|
|
414
|
+
super(message);
|
|
415
|
+
this.name = "OhIntegrityError";
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
class OhDependencyError extends Error {
|
|
420
|
+
constructor(message) {
|
|
421
|
+
super(message);
|
|
422
|
+
this.name = "OhDependencyError";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
class OhProfileError extends Error {
|
|
427
|
+
constructor(message) {
|
|
428
|
+
super(message);
|
|
429
|
+
this.name = "OhProfileError";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
433
|
+
applicationProfileSha256: null,
|
|
434
|
+
capabilities: {
|
|
435
|
+
changesSince: true,
|
|
436
|
+
dependencyClosureExport: true,
|
|
437
|
+
exactSnapshots: true,
|
|
438
|
+
operationReplication: true,
|
|
439
|
+
semanticBundleCommit: true,
|
|
440
|
+
v: 1,
|
|
441
|
+
wholeSpacePurge: false
|
|
442
|
+
},
|
|
443
|
+
profileId: "oh.store.canonical.v1",
|
|
444
|
+
profileKind: "canonical",
|
|
445
|
+
v: 1
|
|
446
|
+
});
|
|
447
|
+
var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
448
|
+
applicationProfileSha256: null,
|
|
449
|
+
capabilities: {
|
|
450
|
+
changesSince: true,
|
|
451
|
+
dependencyClosureExport: true,
|
|
452
|
+
exactSnapshots: true,
|
|
453
|
+
operationReplication: false,
|
|
454
|
+
semanticBundleCommit: true,
|
|
455
|
+
v: 1,
|
|
456
|
+
wholeSpacePurge: true
|
|
457
|
+
},
|
|
458
|
+
profileId: "oh.store.working.v1",
|
|
459
|
+
profileKind: "working",
|
|
460
|
+
v: 1
|
|
461
|
+
});
|
|
462
|
+
var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({
|
|
463
|
+
bytes: 64 * 1024 * 1024,
|
|
464
|
+
records: 8192,
|
|
465
|
+
roots: 1024
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
class OhPurgedSpaceError extends Error {
|
|
469
|
+
receipt;
|
|
470
|
+
constructor(receipt) {
|
|
471
|
+
super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`);
|
|
472
|
+
this.name = "OhPurgedSpaceError";
|
|
473
|
+
this.receipt = receipt;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
var EMPTY_RECORDS_SHA256 = canonicalSha256([]);
|
|
477
|
+
function emptyOhHeadV1() {
|
|
478
|
+
return {
|
|
479
|
+
generation: 0,
|
|
480
|
+
graphRevisionSha256: null,
|
|
481
|
+
operationSha256: null,
|
|
482
|
+
recordsSha256: EMPTY_RECORDS_SHA256,
|
|
483
|
+
sequence: 0,
|
|
484
|
+
v: 1
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function parseOhHeadV1(value) {
|
|
488
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
489
|
+
"generation",
|
|
490
|
+
"graphRevisionSha256",
|
|
491
|
+
"operationSha256",
|
|
492
|
+
"recordsSha256",
|
|
493
|
+
"sequence",
|
|
494
|
+
"v"
|
|
495
|
+
]) || value.v !== 1)
|
|
496
|
+
return null;
|
|
497
|
+
const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256);
|
|
498
|
+
const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
|
|
499
|
+
const recordsSha256 = parseSha256Hex(value.recordsSha256);
|
|
500
|
+
const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null;
|
|
501
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null;
|
|
502
|
+
return generation !== null && sequence !== null && generation === sequence && recordsSha256 !== null && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) && sequence === 0 === (graphRevisionSha256 === null) ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } : null;
|
|
503
|
+
}
|
|
504
|
+
function parseOhHeadRefV1(value) {
|
|
505
|
+
const complete = parseOhHeadV1(value);
|
|
506
|
+
if (complete !== null) {
|
|
507
|
+
return { operationSha256: complete.operationSha256, sequence: complete.sequence };
|
|
508
|
+
}
|
|
509
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"]))
|
|
510
|
+
return null;
|
|
511
|
+
const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
|
|
512
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null;
|
|
513
|
+
return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null;
|
|
514
|
+
}
|
|
515
|
+
function parseCapabilities(value) {
|
|
516
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
517
|
+
"changesSince",
|
|
518
|
+
"dependencyClosureExport",
|
|
519
|
+
"exactSnapshots",
|
|
520
|
+
"operationReplication",
|
|
521
|
+
"semanticBundleCommit",
|
|
522
|
+
"v",
|
|
523
|
+
"wholeSpacePurge"
|
|
524
|
+
]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean")
|
|
525
|
+
return null;
|
|
526
|
+
return {
|
|
527
|
+
changesSince: true,
|
|
528
|
+
dependencyClosureExport: true,
|
|
529
|
+
exactSnapshots: true,
|
|
530
|
+
operationReplication: value.operationReplication,
|
|
531
|
+
semanticBundleCommit: true,
|
|
532
|
+
v: 1,
|
|
533
|
+
wholeSpacePurge: value.wholeSpacePurge
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function createOhStoreProfileV1(input) {
|
|
537
|
+
if (!isPlainRecord(input) || !hasExactKeys(input, [
|
|
538
|
+
"applicationProfileSha256",
|
|
539
|
+
"capabilities",
|
|
540
|
+
"profileId",
|
|
541
|
+
"profileKind",
|
|
542
|
+
"v"
|
|
543
|
+
]) || input.v !== 1)
|
|
544
|
+
throw new TypeError("Invalid Oh store profile input.");
|
|
545
|
+
const profileId = safeCode(input.profileId);
|
|
546
|
+
const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256);
|
|
547
|
+
const capabilities = parseCapabilities(input.capabilities);
|
|
548
|
+
if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") {
|
|
549
|
+
throw new TypeError("Invalid Oh store profile input.");
|
|
550
|
+
}
|
|
551
|
+
if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) {
|
|
552
|
+
throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge.");
|
|
553
|
+
}
|
|
554
|
+
if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) {
|
|
555
|
+
throw new OhProfileError("A canonical profile cannot permit whole-space purge.");
|
|
556
|
+
}
|
|
557
|
+
const payload = {
|
|
558
|
+
applicationProfileSha256,
|
|
559
|
+
capabilities: Object.freeze(capabilities),
|
|
560
|
+
profileId,
|
|
561
|
+
profileKind: input.profileKind,
|
|
562
|
+
v: 1
|
|
563
|
+
};
|
|
564
|
+
return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) });
|
|
565
|
+
}
|
|
566
|
+
function parseOhStoreProfileV1(value) {
|
|
567
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256"))
|
|
568
|
+
return null;
|
|
569
|
+
const digest = parseSha256Hex(value.profileSha256);
|
|
570
|
+
const { profileSha256: _profileSha256, ...input } = value;
|
|
571
|
+
try {
|
|
572
|
+
const created = createOhStoreProfileV1(input);
|
|
573
|
+
return digest !== null && created.profileSha256 === digest ? created : null;
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function createOhStoreBindingV1(input) {
|
|
579
|
+
const profile = parseOhStoreProfileV1(input.profile);
|
|
580
|
+
const realmId = safeCode(input.realmId);
|
|
581
|
+
const spaceId = safeCode(input.spaceId);
|
|
582
|
+
if (input.v !== 1 || profile === null || realmId === null || spaceId === null) {
|
|
583
|
+
throw new TypeError("Invalid Oh store binding input.");
|
|
584
|
+
}
|
|
585
|
+
const payload = {
|
|
586
|
+
contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
587
|
+
profile,
|
|
588
|
+
realmId,
|
|
589
|
+
spaceId,
|
|
590
|
+
v: 1
|
|
591
|
+
};
|
|
592
|
+
return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) });
|
|
593
|
+
}
|
|
594
|
+
function parseOhStoreBindingV1(value) {
|
|
595
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
596
|
+
"bindingSha256",
|
|
597
|
+
"contractSha256",
|
|
598
|
+
"profile",
|
|
599
|
+
"realmId",
|
|
600
|
+
"spaceId",
|
|
601
|
+
"v"
|
|
602
|
+
]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256)
|
|
603
|
+
return null;
|
|
604
|
+
const bindingSha256 = parseSha256Hex(value.bindingSha256);
|
|
605
|
+
const profile = parseOhStoreProfileV1(value.profile);
|
|
606
|
+
try {
|
|
607
|
+
if (bindingSha256 === null || profile === null)
|
|
608
|
+
return null;
|
|
609
|
+
const created = createOhStoreBindingV1({
|
|
610
|
+
profile,
|
|
611
|
+
realmId: value.realmId,
|
|
612
|
+
spaceId: value.spaceId,
|
|
613
|
+
v: 1
|
|
614
|
+
});
|
|
615
|
+
return created.bindingSha256 === bindingSha256 ? created : null;
|
|
616
|
+
} catch {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function sortedRecords(records) {
|
|
621
|
+
return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
|
|
622
|
+
}
|
|
623
|
+
function verifyDependencies(records) {
|
|
624
|
+
for (const record of records.values()) {
|
|
625
|
+
for (const dependency of record.dependencies) {
|
|
626
|
+
if (!records.has(dependency))
|
|
627
|
+
throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
632
|
+
const parsedSpaceId = safeCode(spaceId);
|
|
633
|
+
if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
634
|
+
throw new TypeError("Invalid operation replay input.");
|
|
635
|
+
}
|
|
636
|
+
const records = new Map;
|
|
637
|
+
const operationIds = new Set;
|
|
638
|
+
let head = emptyOhHeadV1();
|
|
639
|
+
for (const value of values) {
|
|
640
|
+
const operation = parseOhOperationV1(value);
|
|
641
|
+
if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) {
|
|
642
|
+
throw new OhIntegrityError("Operation replay chain is broken.");
|
|
643
|
+
}
|
|
644
|
+
operationIds.add(operation.operationId);
|
|
645
|
+
for (const change of operation.changes) {
|
|
646
|
+
if (change.kind === "put")
|
|
647
|
+
records.set(change.record.key, change.record);
|
|
648
|
+
else {
|
|
649
|
+
const prior = records.get(change.key);
|
|
650
|
+
if (prior?.recordSha256 !== change.priorSha256) {
|
|
651
|
+
throw new OhIntegrityError("Replay tombstone does not match its prior record.");
|
|
652
|
+
}
|
|
653
|
+
records.delete(change.key);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (records.size > maximumRecords)
|
|
657
|
+
throw new RangeError("Operation replay exceeds its record bound.");
|
|
658
|
+
verifyDependencies(records);
|
|
659
|
+
const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1);
|
|
660
|
+
const recordsSha256 = canonicalSha256(refs);
|
|
661
|
+
const graphRevisionSha256 = graphRevisionSha256V1({
|
|
662
|
+
changes: operation.changes,
|
|
663
|
+
operationId: operation.operationId,
|
|
664
|
+
parentGraphRevisionSha256: head.graphRevisionSha256,
|
|
665
|
+
recordsSha256,
|
|
666
|
+
revision: operation.sequence
|
|
667
|
+
});
|
|
668
|
+
if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) {
|
|
669
|
+
throw new OhIntegrityError("Replay does not reproduce an operation head.");
|
|
670
|
+
}
|
|
671
|
+
head = {
|
|
672
|
+
generation: operation.sequence,
|
|
673
|
+
graphRevisionSha256,
|
|
674
|
+
operationSha256: operation.operationSha256,
|
|
675
|
+
recordsSha256,
|
|
676
|
+
sequence: operation.sequence,
|
|
677
|
+
v: 1
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return { head, records: sortedRecords(records.values()), v: 1 };
|
|
681
|
+
}
|
|
682
|
+
function transitionOhSnapshotV1(input) {
|
|
683
|
+
const actorId = safeCode(input.actorId);
|
|
684
|
+
const operationId = safeCode(input.operationId);
|
|
685
|
+
const spaceId = safeCode(input.spaceId);
|
|
686
|
+
const instant = parseCanonicalInstantV1(input.instant);
|
|
687
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
688
|
+
if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) {
|
|
689
|
+
throw new TypeError("Invalid graph transition input.");
|
|
690
|
+
}
|
|
691
|
+
const head = parseOhHeadV1(input.snapshot.head);
|
|
692
|
+
if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) {
|
|
693
|
+
throw new OhIntegrityError("The transition snapshot is invalid.");
|
|
694
|
+
}
|
|
695
|
+
const records = new Map;
|
|
696
|
+
for (const value of input.snapshot.records) {
|
|
697
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
698
|
+
if (record === null || records.has(record.key)) {
|
|
699
|
+
throw new OhIntegrityError("The transition snapshot contains an invalid record.");
|
|
700
|
+
}
|
|
701
|
+
records.set(record.key, record);
|
|
702
|
+
}
|
|
703
|
+
verifyDependencies(records);
|
|
704
|
+
const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1));
|
|
705
|
+
if (priorRecordsSha256 !== head.recordsSha256) {
|
|
706
|
+
throw new OhIntegrityError("The transition snapshot does not reproduce its head.");
|
|
707
|
+
}
|
|
708
|
+
for (const change of changes) {
|
|
709
|
+
if (change.kind === "put")
|
|
710
|
+
records.set(change.record.key, change.record);
|
|
711
|
+
else {
|
|
712
|
+
const prior = records.get(change.key);
|
|
713
|
+
if (prior === undefined || prior.recordSha256 !== change.priorSha256) {
|
|
714
|
+
throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`);
|
|
715
|
+
}
|
|
716
|
+
records.delete(change.key);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
720
|
+
throw new RangeError("Graph transition exceeds its record snapshot limit.");
|
|
721
|
+
}
|
|
722
|
+
verifyDependencies(records);
|
|
723
|
+
const nextRecords = sortedRecords(records.values());
|
|
724
|
+
const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1));
|
|
725
|
+
const graphRevisionSha256 = graphRevisionSha256V1({
|
|
726
|
+
changes,
|
|
727
|
+
operationId,
|
|
728
|
+
parentGraphRevisionSha256: head.graphRevisionSha256,
|
|
729
|
+
recordsSha256,
|
|
730
|
+
revision: head.sequence + 1
|
|
731
|
+
});
|
|
732
|
+
const operation = createOhOperationV1({
|
|
733
|
+
actorId,
|
|
734
|
+
changes,
|
|
735
|
+
contractId: OH_CONTRACT_MANIFEST_V1.contractId,
|
|
736
|
+
graphRevisionSha256,
|
|
737
|
+
instant,
|
|
738
|
+
operationId,
|
|
739
|
+
parentOperationSha256: head.operationSha256,
|
|
740
|
+
recordsSha256,
|
|
741
|
+
sequence: head.sequence + 1,
|
|
742
|
+
spaceId,
|
|
743
|
+
v: 1
|
|
744
|
+
});
|
|
745
|
+
const nextHead = {
|
|
746
|
+
generation: operation.sequence,
|
|
747
|
+
graphRevisionSha256,
|
|
748
|
+
operationSha256: operation.operationSha256,
|
|
749
|
+
recordsSha256,
|
|
750
|
+
sequence: operation.sequence,
|
|
751
|
+
v: 1
|
|
752
|
+
};
|
|
753
|
+
return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } };
|
|
754
|
+
}
|
|
755
|
+
function normalizeRoots(values) {
|
|
756
|
+
if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) {
|
|
757
|
+
throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`);
|
|
758
|
+
}
|
|
759
|
+
const roots = values.map((value) => safeCode(value, 512));
|
|
760
|
+
if (roots.some((value) => value === null))
|
|
761
|
+
throw new TypeError("Invalid dependency closure root.");
|
|
762
|
+
const sorted = [...roots].sort();
|
|
763
|
+
if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) {
|
|
764
|
+
throw new TypeError("Dependency closure roots must be unique.");
|
|
765
|
+
}
|
|
766
|
+
return sorted;
|
|
767
|
+
}
|
|
768
|
+
function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) {
|
|
769
|
+
const selected = new Map;
|
|
770
|
+
const pending = [...roots];
|
|
771
|
+
let selectedBytes = 0;
|
|
772
|
+
while (pending.length > 0) {
|
|
773
|
+
const key = pending.pop();
|
|
774
|
+
if (selected.has(key))
|
|
775
|
+
continue;
|
|
776
|
+
const record = available.get(key);
|
|
777
|
+
if (record === undefined)
|
|
778
|
+
throw new OhDependencyError(`Dependency closure record ${key} is missing.`);
|
|
779
|
+
selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1;
|
|
780
|
+
if (selectedBytes > maximumBytes)
|
|
781
|
+
throw new RangeError("Dependency closure exceeds its canonical byte bound.");
|
|
782
|
+
selected.set(key, record);
|
|
783
|
+
if (selected.size > maximumRecords)
|
|
784
|
+
throw new RangeError("Dependency closure exceeds its record bound.");
|
|
785
|
+
pending.push(...record.dependencies);
|
|
786
|
+
}
|
|
787
|
+
return sortedRecords(selected.values());
|
|
788
|
+
}
|
|
789
|
+
function createOhDependencyClosureV1(input) {
|
|
790
|
+
const binding = parseOhStoreBindingV1(input.binding);
|
|
791
|
+
const head = parseOhHeadV1(input.snapshot.head);
|
|
792
|
+
const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records;
|
|
793
|
+
if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) {
|
|
794
|
+
throw new TypeError("Invalid dependency closure input.");
|
|
795
|
+
}
|
|
796
|
+
const roots = normalizeRoots(input.roots);
|
|
797
|
+
const available = new Map;
|
|
798
|
+
for (const value of input.snapshot.records) {
|
|
799
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
800
|
+
if (record === null || available.has(record.key))
|
|
801
|
+
throw new OhIntegrityError("Snapshot contains an invalid record.");
|
|
802
|
+
available.set(record.key, record);
|
|
803
|
+
}
|
|
804
|
+
const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1));
|
|
805
|
+
if (recordsSha256 !== head.recordsSha256)
|
|
806
|
+
throw new OhIntegrityError("Snapshot records do not reproduce its head.");
|
|
807
|
+
const records = closureRecords(available, roots, maximumRecords);
|
|
808
|
+
const payload = { binding, head, records, roots, v: 1 };
|
|
809
|
+
const closure = { ...payload, closureSha256: canonicalSha256(payload) };
|
|
810
|
+
if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) {
|
|
811
|
+
throw new RangeError("Dependency closure exceeds its canonical byte bound.");
|
|
812
|
+
}
|
|
813
|
+
return Object.freeze(closure);
|
|
814
|
+
}
|
|
815
|
+
function parseOhDependencyClosureV1(value) {
|
|
816
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
817
|
+
"binding",
|
|
818
|
+
"closureSha256",
|
|
819
|
+
"head",
|
|
820
|
+
"records",
|
|
821
|
+
"roots",
|
|
822
|
+
"v"
|
|
823
|
+
]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records)
|
|
824
|
+
return null;
|
|
825
|
+
const binding = parseOhStoreBindingV1(value.binding);
|
|
826
|
+
const head = parseOhHeadV1(value.head);
|
|
827
|
+
const closureSha256 = parseSha256Hex(value.closureSha256);
|
|
828
|
+
if (binding === null || head === null || closureSha256 === null)
|
|
829
|
+
return null;
|
|
830
|
+
const records = new Map;
|
|
831
|
+
for (const item of value.records) {
|
|
832
|
+
const record = parseKnowledgeGraphRecordV1(item);
|
|
833
|
+
if (record === null || records.has(record.key))
|
|
834
|
+
return null;
|
|
835
|
+
records.set(record.key, record);
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
const roots = normalizeRoots(value.roots);
|
|
839
|
+
if (canonicalJson(roots) !== canonicalJson(value.roots))
|
|
840
|
+
return null;
|
|
841
|
+
const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records);
|
|
842
|
+
if (canonicalJson(exact) !== canonicalJson(value.records))
|
|
843
|
+
return null;
|
|
844
|
+
const payload = { binding, head, records: exact, roots, v: 1 };
|
|
845
|
+
const parsed = { ...payload, closureSha256 };
|
|
846
|
+
return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null;
|
|
847
|
+
} catch {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function verifyOhDependencyClosureV1(value) {
|
|
852
|
+
const closure = parseOhDependencyClosureV1(value);
|
|
853
|
+
return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true };
|
|
854
|
+
}
|
|
855
|
+
function verifyOhDependencyClosureAgainstV1(value, expected) {
|
|
856
|
+
const binding = parseOhStoreBindingV1(expected.binding);
|
|
857
|
+
const head = parseOhHeadV1(expected.head);
|
|
858
|
+
if (binding === null || head === null)
|
|
859
|
+
return { ok: false, reason: "invalid-expectation" };
|
|
860
|
+
const closure = parseOhDependencyClosureV1(value);
|
|
861
|
+
if (closure === null)
|
|
862
|
+
return { ok: false, reason: "invalid-closure" };
|
|
863
|
+
if (closure.binding.bindingSha256 !== binding.bindingSha256)
|
|
864
|
+
return { ok: false, reason: "binding-mismatch" };
|
|
865
|
+
if (canonicalJson(closure.head) !== canonicalJson(head))
|
|
866
|
+
return { ok: false, reason: "head-mismatch" };
|
|
867
|
+
return { closure, ok: true, verification: "expected-authority-and-head" };
|
|
868
|
+
}
|
|
869
|
+
function createOhSpacePurgeReceiptV1(input) {
|
|
870
|
+
const binding = parseOhStoreBindingV1(input.binding);
|
|
871
|
+
const priorHead = parseOhHeadV1(input.priorHead);
|
|
872
|
+
const purgedAt = parseCanonicalInstantV1(input.purgedAt);
|
|
873
|
+
if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) {
|
|
874
|
+
throw new OhProfileError("Only a bound working realm can produce a purge receipt.");
|
|
875
|
+
}
|
|
876
|
+
const payload = {
|
|
877
|
+
bindingSha256: binding.bindingSha256,
|
|
878
|
+
priorHead,
|
|
879
|
+
purgedAt,
|
|
880
|
+
spaceId: binding.spaceId,
|
|
881
|
+
v: 1
|
|
882
|
+
};
|
|
883
|
+
return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) });
|
|
884
|
+
}
|
|
885
|
+
function parseOhSpacePurgeReceiptV1(value) {
|
|
886
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
887
|
+
"bindingSha256",
|
|
888
|
+
"priorHead",
|
|
889
|
+
"purgedAt",
|
|
890
|
+
"receiptSha256",
|
|
891
|
+
"spaceId",
|
|
892
|
+
"v"
|
|
893
|
+
]) || value.v !== 1)
|
|
894
|
+
return null;
|
|
895
|
+
const bindingSha256 = parseSha256Hex(value.bindingSha256);
|
|
896
|
+
const priorHead = parseOhHeadV1(value.priorHead);
|
|
897
|
+
const purgedAt = parseCanonicalInstantV1(value.purgedAt);
|
|
898
|
+
const receiptSha256 = parseSha256Hex(value.receiptSha256);
|
|
899
|
+
const spaceId = safeCode(value.spaceId);
|
|
900
|
+
if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null)
|
|
901
|
+
return null;
|
|
902
|
+
const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 };
|
|
903
|
+
return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
class OhSemanticBundleIngressV1 {
|
|
907
|
+
#codecs;
|
|
908
|
+
#store;
|
|
909
|
+
constructor(store, codecs) {
|
|
910
|
+
this.#store = store;
|
|
911
|
+
this.#codecs = codecs.seal();
|
|
912
|
+
}
|
|
913
|
+
async commit(value) {
|
|
914
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
915
|
+
"actorId",
|
|
916
|
+
"expectedHead",
|
|
917
|
+
"instant",
|
|
918
|
+
"operationId",
|
|
919
|
+
"puts",
|
|
920
|
+
"tombstones",
|
|
921
|
+
"v"
|
|
922
|
+
]) || value.v !== 1 || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) || value.puts.length + value.tombstones.length < 1 || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) {
|
|
923
|
+
throw new TypeError("Invalid semantic bundle.");
|
|
924
|
+
}
|
|
925
|
+
const actorId = safeCode(value.actorId);
|
|
926
|
+
const operationId = safeCode(value.operationId);
|
|
927
|
+
const expected = value.expectedHead;
|
|
928
|
+
const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant);
|
|
929
|
+
if (actorId === null || operationId === null || !isPlainRecord(expected) || !hasExactKeys(expected, ["generation", "operationSha256"]) || !Number.isSafeInteger(expected.generation) || expected.generation < 0 || expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null || expected.generation === 0 !== (expected.operationSha256 === null) || value.instant !== null && instant === null)
|
|
930
|
+
throw new TypeError("Invalid semantic bundle identity.");
|
|
931
|
+
const changes = [];
|
|
932
|
+
for (const item of value.puts) {
|
|
933
|
+
if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) || item.v !== 1 || !Array.isArray(item.dependencies) || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) {
|
|
934
|
+
throw new TypeError("Invalid semantic bundle put.");
|
|
935
|
+
}
|
|
936
|
+
const parsed = this.#codecs.parseRequired(item.kind, item.value);
|
|
937
|
+
if (parsed === null)
|
|
938
|
+
throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`);
|
|
939
|
+
const record = createKnowledgeGraphRecordV1({
|
|
940
|
+
dependencies: item.dependencies,
|
|
941
|
+
key: item.key,
|
|
942
|
+
kind: item.kind,
|
|
943
|
+
v: 1,
|
|
944
|
+
value: parsed
|
|
945
|
+
});
|
|
946
|
+
changes.push({ kind: "put", record, v: 1 });
|
|
947
|
+
}
|
|
948
|
+
for (const item of value.tombstones) {
|
|
949
|
+
if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) {
|
|
950
|
+
throw new TypeError("Invalid semantic bundle tombstone.");
|
|
951
|
+
}
|
|
952
|
+
const priorSha256 = parseSha256Hex(item.priorSha256);
|
|
953
|
+
if (typeof item.key !== "string" || priorSha256 === null)
|
|
954
|
+
throw new TypeError("Invalid semantic bundle tombstone.");
|
|
955
|
+
changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 });
|
|
956
|
+
}
|
|
957
|
+
const canonical = canonicalKnowledgeGraphChangesV1(changes);
|
|
958
|
+
return await this.#store.commit({
|
|
959
|
+
actorId,
|
|
960
|
+
changes: canonical,
|
|
961
|
+
expectedHead: {
|
|
962
|
+
generation: expected.generation,
|
|
963
|
+
operationSha256: expected.operationSha256
|
|
964
|
+
},
|
|
965
|
+
...typeof instant === "string" ? { instant } : {},
|
|
966
|
+
operationId
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/libsql.ts
|
|
972
|
+
var OH_LIBSQL_STORE_LIMITS_V1 = Object.freeze({
|
|
973
|
+
changesPerCommit: 64,
|
|
974
|
+
changeFeedLimit: 7,
|
|
975
|
+
dependenciesPerCommit: 512,
|
|
976
|
+
historyBytes: 4 * 1024 * 1024,
|
|
977
|
+
historyOperations: 16384,
|
|
978
|
+
operationBytes: 512 * 1024,
|
|
979
|
+
providerResponseBytes: 9000000,
|
|
980
|
+
snapshotComponentBytes: 6 * 1024 * 1024
|
|
981
|
+
});
|
|
982
|
+
var AUTHORITY_SCHEMA_NAME = "oh.libsql-authority.v1";
|
|
983
|
+
var AUTHORITY_SCHEMA_VERSION = 1;
|
|
984
|
+
var EMPTY_RECORDS_SHA2562 = canonicalSha256([]);
|
|
985
|
+
var PURGE_ROW_SELECT = `SELECT space_id, binding_sha256, prior_operation_sha256,
|
|
986
|
+
prior_sequence, purged_at, receipt_sha256, receipt_json
|
|
987
|
+
FROM oh_authority_purges WHERE space_id = ?`;
|
|
988
|
+
var BINDING_ROW_SELECT = `SELECT space_id, realm_id, profile_id, profile_kind,
|
|
989
|
+
profile_sha256, binding_sha256, binding_json FROM oh_authority_bindings WHERE space_id = ?`;
|
|
990
|
+
var SPACE_PURGE_PROOF_SELECT = `SELECT generation, graph_revision_sha256, head_operation_sha256,
|
|
991
|
+
records_sha256, sequence, contract_id FROM oh_authority_spaces WHERE space_id = ?`;
|
|
992
|
+
var OPERATION_ROW_COLUMNS = `operation_sha256, space_id, sequence, operation_id,
|
|
993
|
+
parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`;
|
|
994
|
+
var OPERATION_RESPONSE_BYTES = `2 * length(CAST(operation.operation_json AS BLOB))
|
|
995
|
+
+ 2 * (length(operation.operation_sha256) + length(operation.space_id)
|
|
996
|
+
+ length(operation.operation_id) + coalesce(length(operation.parent_operation_sha256), 0)
|
|
997
|
+
+ length(operation.graph_revision_sha256) + length(operation.records_sha256)
|
|
998
|
+
+ length(operation.instant)) + 512`;
|
|
999
|
+
var RECORD_RESPONSE_BYTES = `2 * length(CAST(record.record_json AS BLOB))
|
|
1000
|
+
+ 2 * (length(record.record_key) + length(record.kind) + length(record.record_sha256)
|
|
1001
|
+
+ length(record.operation_sha256)) + 384`;
|
|
1002
|
+
var DEPENDENCY_RESPONSE_BYTES = `2 * (length(dependency.record_key)
|
|
1003
|
+
+ length(dependency.dependency_key)) + 192`;
|
|
1004
|
+
var OPERATION_RECORD_RESPONSE_BYTES = `2 * (length(materialized.space_id)
|
|
1005
|
+
+ length(materialized.operation_sha256) + length(materialized.record_key)
|
|
1006
|
+
+ length(materialized.change_kind) + length(materialized.record_sha256)) + 320`;
|
|
1007
|
+
var AUTHORITY_SCHEMA_TABLE_STATEMENT = `CREATE TABLE IF NOT EXISTS oh_authority_schemas (
|
|
1008
|
+
version INTEGER PRIMARY KEY,
|
|
1009
|
+
name TEXT NOT NULL UNIQUE,
|
|
1010
|
+
schema_sha256 TEXT NOT NULL,
|
|
1011
|
+
applied_at TEXT NOT NULL
|
|
1012
|
+
) STRICT`;
|
|
1013
|
+
var AUTHORITY_SCHEMA_STATEMENTS = Object.freeze([
|
|
1014
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_contracts (
|
|
1015
|
+
contract_id TEXT PRIMARY KEY,
|
|
1016
|
+
contract_sha256 TEXT NOT NULL,
|
|
1017
|
+
manifest_json TEXT NOT NULL
|
|
1018
|
+
) STRICT`,
|
|
1019
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_spaces (
|
|
1020
|
+
space_id TEXT PRIMARY KEY,
|
|
1021
|
+
contract_id TEXT NOT NULL,
|
|
1022
|
+
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
1023
|
+
head_operation_sha256 TEXT,
|
|
1024
|
+
graph_revision_sha256 TEXT,
|
|
1025
|
+
records_sha256 TEXT NOT NULL,
|
|
1026
|
+
sequence INTEGER NOT NULL CHECK(sequence >= 0),
|
|
1027
|
+
created_at TEXT NOT NULL,
|
|
1028
|
+
updated_at TEXT NOT NULL,
|
|
1029
|
+
CHECK(generation = sequence)
|
|
1030
|
+
) STRICT`,
|
|
1031
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_operations (
|
|
1032
|
+
operation_sha256 TEXT PRIMARY KEY,
|
|
1033
|
+
space_id TEXT NOT NULL,
|
|
1034
|
+
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
1035
|
+
operation_id TEXT NOT NULL,
|
|
1036
|
+
parent_operation_sha256 TEXT,
|
|
1037
|
+
graph_revision_sha256 TEXT NOT NULL,
|
|
1038
|
+
records_sha256 TEXT NOT NULL,
|
|
1039
|
+
operation_json TEXT NOT NULL,
|
|
1040
|
+
instant TEXT NOT NULL,
|
|
1041
|
+
UNIQUE(space_id, sequence),
|
|
1042
|
+
UNIQUE(space_id, operation_id)
|
|
1043
|
+
) STRICT`,
|
|
1044
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_operation_records (
|
|
1045
|
+
space_id TEXT NOT NULL,
|
|
1046
|
+
operation_sha256 TEXT NOT NULL,
|
|
1047
|
+
ordinal INTEGER NOT NULL CHECK(ordinal >= 0),
|
|
1048
|
+
record_key TEXT NOT NULL,
|
|
1049
|
+
change_kind TEXT NOT NULL CHECK(change_kind IN ('put', 'tombstone')),
|
|
1050
|
+
record_sha256 TEXT NOT NULL,
|
|
1051
|
+
PRIMARY KEY(operation_sha256, ordinal),
|
|
1052
|
+
UNIQUE(operation_sha256, record_key)
|
|
1053
|
+
) STRICT`,
|
|
1054
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_records (
|
|
1055
|
+
space_id TEXT NOT NULL,
|
|
1056
|
+
record_key TEXT NOT NULL,
|
|
1057
|
+
kind TEXT NOT NULL,
|
|
1058
|
+
record_sha256 TEXT NOT NULL,
|
|
1059
|
+
record_json TEXT NOT NULL,
|
|
1060
|
+
operation_sha256 TEXT NOT NULL,
|
|
1061
|
+
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
1062
|
+
PRIMARY KEY(space_id, record_key)
|
|
1063
|
+
) STRICT`,
|
|
1064
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_dependencies (
|
|
1065
|
+
space_id TEXT NOT NULL,
|
|
1066
|
+
record_key TEXT NOT NULL,
|
|
1067
|
+
dependency_key TEXT NOT NULL,
|
|
1068
|
+
PRIMARY KEY(space_id, record_key, dependency_key)
|
|
1069
|
+
) STRICT`,
|
|
1070
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_bindings (
|
|
1071
|
+
space_id TEXT PRIMARY KEY,
|
|
1072
|
+
realm_id TEXT NOT NULL,
|
|
1073
|
+
profile_id TEXT NOT NULL,
|
|
1074
|
+
profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')),
|
|
1075
|
+
profile_sha256 TEXT NOT NULL,
|
|
1076
|
+
binding_sha256 TEXT NOT NULL UNIQUE,
|
|
1077
|
+
binding_json TEXT NOT NULL,
|
|
1078
|
+
created_at TEXT NOT NULL
|
|
1079
|
+
) STRICT`,
|
|
1080
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_purges (
|
|
1081
|
+
space_id TEXT PRIMARY KEY,
|
|
1082
|
+
binding_sha256 TEXT NOT NULL,
|
|
1083
|
+
prior_operation_sha256 TEXT,
|
|
1084
|
+
prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0),
|
|
1085
|
+
purged_at TEXT NOT NULL,
|
|
1086
|
+
receipt_sha256 TEXT NOT NULL UNIQUE,
|
|
1087
|
+
receipt_json TEXT NOT NULL
|
|
1088
|
+
) STRICT`,
|
|
1089
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_commit_guards (
|
|
1090
|
+
value TEXT NOT NULL CHECK(value = 'ok')
|
|
1091
|
+
) STRICT`,
|
|
1092
|
+
"CREATE INDEX IF NOT EXISTS oh_authority_operations_space_sequence ON oh_authority_operations(space_id, sequence)",
|
|
1093
|
+
"CREATE INDEX IF NOT EXISTS oh_authority_records_space_kind ON oh_authority_records(space_id, kind, record_key)",
|
|
1094
|
+
"CREATE INDEX IF NOT EXISTS oh_authority_dependencies_dependency ON oh_authority_dependencies(space_id, dependency_key)",
|
|
1095
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operations_no_update
|
|
1096
|
+
BEFORE UPDATE ON oh_authority_operations
|
|
1097
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operations are immutable'); END`,
|
|
1098
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operations_guard_delete
|
|
1099
|
+
BEFORE DELETE ON oh_authority_operations
|
|
1100
|
+
WHEN NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = OLD.space_id)
|
|
1101
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operations require a purge receipt'); END`,
|
|
1102
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_no_update
|
|
1103
|
+
BEFORE UPDATE ON oh_authority_operation_records
|
|
1104
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operation records are immutable'); END`,
|
|
1105
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_insert
|
|
1106
|
+
BEFORE INSERT ON oh_authority_operation_records
|
|
1107
|
+
WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations
|
|
1108
|
+
WHERE operation_sha256 = NEW.operation_sha256 AND space_id = NEW.space_id)
|
|
1109
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operation record has no owning operation'); END`,
|
|
1110
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_delete
|
|
1111
|
+
BEFORE DELETE ON oh_authority_operation_records
|
|
1112
|
+
WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations AS operation
|
|
1113
|
+
JOIN oh_authority_purges AS purge ON purge.space_id = operation.space_id
|
|
1114
|
+
WHERE operation.operation_sha256 = OLD.operation_sha256)
|
|
1115
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operation records require a purge receipt'); END`,
|
|
1116
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_update
|
|
1117
|
+
BEFORE UPDATE ON oh_authority_purges
|
|
1118
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END`,
|
|
1119
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_delete
|
|
1120
|
+
BEFORE DELETE ON oh_authority_purges
|
|
1121
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END`
|
|
1122
|
+
]);
|
|
1123
|
+
function normalizedSchemaSql(sql) {
|
|
1124
|
+
return sql.replace(/\bIF\s+NOT\s+EXISTS\b/giu, "").replace(/\s+/gu, " ").trim();
|
|
1125
|
+
}
|
|
1126
|
+
function expectedSchemaObject(statement) {
|
|
1127
|
+
const match = /^CREATE\s+(TABLE|INDEX|TRIGGER)(?:\s+IF\s+NOT\s+EXISTS)?\s+([a-z0-9_]+)/iu.exec(statement.trim());
|
|
1128
|
+
if (match === null)
|
|
1129
|
+
throw new Error("Invalid compiled authority schema statement.");
|
|
1130
|
+
const declaredType = match[1]?.toLowerCase();
|
|
1131
|
+
const type = declaredType === "index" ? "index" : declaredType === "trigger" ? "trigger" : "table";
|
|
1132
|
+
const name = match[2];
|
|
1133
|
+
const tableMatch = type === "index" || type === "trigger" ? /\bON\s+([a-z0-9_]+)/iu.exec(statement) : null;
|
|
1134
|
+
const tableName = type === "table" ? name : tableMatch?.[1];
|
|
1135
|
+
if (tableName === undefined)
|
|
1136
|
+
throw new Error("Invalid compiled authority index statement.");
|
|
1137
|
+
return { name, sql: normalizedSchemaSql(statement), tableName, type };
|
|
1138
|
+
}
|
|
1139
|
+
var AUTHORITY_SCHEMA_OBJECTS = Object.freeze([AUTHORITY_SCHEMA_TABLE_STATEMENT, ...AUTHORITY_SCHEMA_STATEMENTS].map(expectedSchemaObject).sort((left, right) => canonicalJson([left.type, left.name]).localeCompare(canonicalJson([right.type, right.name]))));
|
|
1140
|
+
var AUTHORITY_SCHEMA_SHA256 = canonicalSha256(AUTHORITY_SCHEMA_OBJECTS);
|
|
1141
|
+
function rowValue(row, key, index) {
|
|
1142
|
+
return Array.isArray(row) ? row[index] : row[key];
|
|
1143
|
+
}
|
|
1144
|
+
function integer(value) {
|
|
1145
|
+
if (typeof value === "number")
|
|
1146
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
1147
|
+
if (typeof value === "bigint") {
|
|
1148
|
+
const parsed = Number(value);
|
|
1149
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
1150
|
+
}
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
function normalizeLimit(value, fallback = 100, maximum = 1000) {
|
|
1154
|
+
const limit = value ?? fallback;
|
|
1155
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > maximum) {
|
|
1156
|
+
throw new RangeError(`limit must be an integer from 1 through ${maximum}.`);
|
|
1157
|
+
}
|
|
1158
|
+
return limit;
|
|
1159
|
+
}
|
|
1160
|
+
function parseOperationJson(value) {
|
|
1161
|
+
if (typeof value !== "string")
|
|
1162
|
+
throw new OhIntegrityError("A stored operation is not JSON text.");
|
|
1163
|
+
let parsedValue;
|
|
1164
|
+
try {
|
|
1165
|
+
parsedValue = JSON.parse(value);
|
|
1166
|
+
} catch {
|
|
1167
|
+
throw new OhIntegrityError("A stored operation is not JSON.");
|
|
1168
|
+
}
|
|
1169
|
+
const operation = parseOhOperationV1(parsedValue);
|
|
1170
|
+
if (operation === null || canonicalJson(operation) !== value) {
|
|
1171
|
+
throw new OhIntegrityError("A stored operation is invalid.");
|
|
1172
|
+
}
|
|
1173
|
+
return operation;
|
|
1174
|
+
}
|
|
1175
|
+
function parseOperationRow(row, expected = {}) {
|
|
1176
|
+
const operation = parseOperationJson(rowValue(row, "operation_json", 7));
|
|
1177
|
+
if (rowValue(row, "operation_sha256", 0) !== operation.operationSha256 || rowValue(row, "space_id", 1) !== operation.spaceId || integer(rowValue(row, "sequence", 2)) !== operation.sequence || rowValue(row, "operation_id", 3) !== operation.operationId || rowValue(row, "parent_operation_sha256", 4) !== operation.parentOperationSha256 || rowValue(row, "graph_revision_sha256", 5) !== operation.graphRevisionSha256 || rowValue(row, "records_sha256", 6) !== operation.recordsSha256 || rowValue(row, "instant", 8) !== operation.instant || expected.spaceId !== undefined && operation.spaceId !== expected.spaceId || expected.operationId !== undefined && operation.operationId !== expected.operationId || expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256) {
|
|
1178
|
+
throw new OhIntegrityError("Remote operation columns do not match their canonical envelope.");
|
|
1179
|
+
}
|
|
1180
|
+
return operation;
|
|
1181
|
+
}
|
|
1182
|
+
function parseBindingRow(row, expectedSpaceId) {
|
|
1183
|
+
const json = rowValue(row, "binding_json", 6);
|
|
1184
|
+
if (typeof json !== "string")
|
|
1185
|
+
throw new OhIntegrityError("A remote store binding is not JSON text.");
|
|
1186
|
+
let value;
|
|
1187
|
+
try {
|
|
1188
|
+
value = JSON.parse(json);
|
|
1189
|
+
} catch {
|
|
1190
|
+
throw new OhIntegrityError("A remote store binding is not JSON.");
|
|
1191
|
+
}
|
|
1192
|
+
const binding = parseOhStoreBindingV1(value);
|
|
1193
|
+
if (binding === null || canonicalJson(binding) !== json || binding.spaceId !== expectedSpaceId || rowValue(row, "space_id", 0) !== binding.spaceId || rowValue(row, "realm_id", 1) !== binding.realmId || rowValue(row, "profile_id", 2) !== binding.profile.profileId || rowValue(row, "profile_kind", 3) !== binding.profile.profileKind || rowValue(row, "profile_sha256", 4) !== binding.profile.profileSha256 || rowValue(row, "binding_sha256", 5) !== binding.bindingSha256) {
|
|
1194
|
+
throw new OhIntegrityError("Remote binding columns do not match their canonical envelope.");
|
|
1195
|
+
}
|
|
1196
|
+
return binding;
|
|
1197
|
+
}
|
|
1198
|
+
function parsePurgeReceiptRow(row, expectedSpaceId, expectedBindingSha256) {
|
|
1199
|
+
const json = rowValue(row, "receipt_json", 6);
|
|
1200
|
+
if (typeof json !== "string")
|
|
1201
|
+
throw new OhIntegrityError("A remote purge receipt is invalid.");
|
|
1202
|
+
let value;
|
|
1203
|
+
try {
|
|
1204
|
+
value = JSON.parse(json);
|
|
1205
|
+
} catch {
|
|
1206
|
+
throw new OhIntegrityError("A remote purge receipt is invalid.");
|
|
1207
|
+
}
|
|
1208
|
+
const receipt = parseOhSpacePurgeReceiptV1(value);
|
|
1209
|
+
if (receipt === null || canonicalJson(receipt) !== json) {
|
|
1210
|
+
throw new OhIntegrityError("A remote purge receipt is invalid.");
|
|
1211
|
+
}
|
|
1212
|
+
if (receipt.spaceId !== expectedSpaceId || expectedBindingSha256 !== undefined && receipt.bindingSha256 !== expectedBindingSha256 || rowValue(row, "space_id", 0) !== receipt.spaceId || rowValue(row, "binding_sha256", 1) !== receipt.bindingSha256 || rowValue(row, "prior_operation_sha256", 2) !== receipt.priorHead.operationSha256 || integer(rowValue(row, "prior_sequence", 3)) !== receipt.priorHead.sequence || rowValue(row, "purged_at", 4) !== receipt.purgedAt || rowValue(row, "receipt_sha256", 5) !== receipt.receiptSha256) {
|
|
1213
|
+
throw new OhIntegrityError("Remote purge columns do not match their canonical receipt.");
|
|
1214
|
+
}
|
|
1215
|
+
return receipt;
|
|
1216
|
+
}
|
|
1217
|
+
function parseHeadRow(row) {
|
|
1218
|
+
const generation = integer(rowValue(row, "generation", 0));
|
|
1219
|
+
const graphValue = rowValue(row, "graph_revision_sha256", 1);
|
|
1220
|
+
const operationValue = rowValue(row, "head_operation_sha256", 2);
|
|
1221
|
+
const recordsSha256 = parseSha256Hex(rowValue(row, "records_sha256", 3));
|
|
1222
|
+
const sequence = integer(rowValue(row, "sequence", 4));
|
|
1223
|
+
const graphRevisionSha256 = graphValue === null ? null : parseSha256Hex(graphValue);
|
|
1224
|
+
const operationSha256 = operationValue === null ? null : parseSha256Hex(operationValue);
|
|
1225
|
+
if (generation === null || sequence === null || generation !== sequence || recordsSha256 === null || graphValue !== null && graphRevisionSha256 === null || operationValue !== null && operationSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null)) {
|
|
1226
|
+
throw new OhIntegrityError("The remote authority contains an invalid head.");
|
|
1227
|
+
}
|
|
1228
|
+
return { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 };
|
|
1229
|
+
}
|
|
1230
|
+
async function queryOne(client, statement) {
|
|
1231
|
+
return (await client.execute(statement)).rows[0] ?? null;
|
|
1232
|
+
}
|
|
1233
|
+
async function verifyAuthoritySchemaObjects(client) {
|
|
1234
|
+
const rows = (await client.execute({ sql: `SELECT type, name, tbl_name, sql FROM sqlite_schema
|
|
1235
|
+
WHERE sql IS NOT NULL AND (name = 'oh_authority_schemas' OR name GLOB 'oh_authority_*'
|
|
1236
|
+
OR tbl_name GLOB 'oh_authority_*')
|
|
1237
|
+
ORDER BY type, name` })).rows;
|
|
1238
|
+
const actual = rows.map((row) => {
|
|
1239
|
+
const type = rowValue(row, "type", 0);
|
|
1240
|
+
const name = rowValue(row, "name", 1);
|
|
1241
|
+
const tableName = rowValue(row, "tbl_name", 2);
|
|
1242
|
+
const sql = rowValue(row, "sql", 3);
|
|
1243
|
+
if (type !== "table" && type !== "index" && type !== "trigger" || typeof name !== "string" || typeof tableName !== "string" || typeof sql !== "string") {
|
|
1244
|
+
throw new OhIntegrityError("The installed libSQL authority has an invalid schema object.");
|
|
1245
|
+
}
|
|
1246
|
+
return { name, sql: normalizedSchemaSql(sql), tableName, type };
|
|
1247
|
+
});
|
|
1248
|
+
if (canonicalJson(actual) !== canonicalJson(AUTHORITY_SCHEMA_OBJECTS)) {
|
|
1249
|
+
throw new OhIntegrityError("The installed libSQL authority objects differ from this runtime.");
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
async function verifyAuthoritySchema(client) {
|
|
1253
|
+
const installed = await queryOne(client, { sql: `SELECT name, schema_sha256
|
|
1254
|
+
FROM oh_authority_schemas WHERE version = ?`, args: [AUTHORITY_SCHEMA_VERSION] });
|
|
1255
|
+
if (installed === null || rowValue(installed, "name", 0) !== AUTHORITY_SCHEMA_NAME || rowValue(installed, "schema_sha256", 1) !== AUTHORITY_SCHEMA_SHA256) {
|
|
1256
|
+
throw new OhIntegrityError("The installed libSQL authority schema differs from this runtime.");
|
|
1257
|
+
}
|
|
1258
|
+
const contract = await queryOne(client, { sql: `SELECT contract_sha256, manifest_json
|
|
1259
|
+
FROM oh_authority_contracts WHERE contract_id = ?`, args: [OH_CONTRACT_MANIFEST_V1.contractId] });
|
|
1260
|
+
if (contract === null || rowValue(contract, "contract_sha256", 0) !== OH_CONTRACT_MANIFEST_V1.contractSha256 || rowValue(contract, "manifest_json", 1) !== canonicalJson(OH_CONTRACT_MANIFEST_V1)) {
|
|
1261
|
+
throw new OhIntegrityError("The remote authority contract differs from this runtime.");
|
|
1262
|
+
}
|
|
1263
|
+
await verifyAuthoritySchemaObjects(client);
|
|
1264
|
+
}
|
|
1265
|
+
async function bootstrapOhLibSqlAuthorityV1(client) {
|
|
1266
|
+
const existingObjects = (await client.execute({ sql: `SELECT name FROM sqlite_schema
|
|
1267
|
+
WHERE sql IS NOT NULL AND (name = 'oh_authority_schemas' OR name GLOB 'oh_authority_*'
|
|
1268
|
+
OR tbl_name GLOB 'oh_authority_*')` })).rows;
|
|
1269
|
+
if (existingObjects.length > 0) {
|
|
1270
|
+
if (!existingObjects.some((row) => rowValue(row, "name", 0) === "oh_authority_schemas")) {
|
|
1271
|
+
throw new OhIntegrityError("Refusing to bootstrap over preexisting Oh authority objects.");
|
|
1272
|
+
}
|
|
1273
|
+
await verifyAuthoritySchema(client);
|
|
1274
|
+
return { schemaSha256: AUTHORITY_SCHEMA_SHA256, schemaVersion: 1, v: 1 };
|
|
1275
|
+
}
|
|
1276
|
+
const setup = [AUTHORITY_SCHEMA_TABLE_STATEMENT, ...AUTHORITY_SCHEMA_STATEMENTS].map((sql) => ({ sql }));
|
|
1277
|
+
setup.push({
|
|
1278
|
+
sql: `INSERT INTO oh_authority_schemas(version, name, schema_sha256, applied_at)
|
|
1279
|
+
VALUES (?, ?, ?, ?) ON CONFLICT(version) DO NOTHING`,
|
|
1280
|
+
args: [AUTHORITY_SCHEMA_VERSION, AUTHORITY_SCHEMA_NAME, AUTHORITY_SCHEMA_SHA256, canonicalNow()]
|
|
1281
|
+
});
|
|
1282
|
+
setup.push({ sql: `INSERT INTO oh_authority_contracts(contract_id, contract_sha256, manifest_json)
|
|
1283
|
+
VALUES (?, ?, ?) ON CONFLICT(contract_id) DO NOTHING`, args: [
|
|
1284
|
+
OH_CONTRACT_MANIFEST_V1.contractId,
|
|
1285
|
+
OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
1286
|
+
canonicalJson(OH_CONTRACT_MANIFEST_V1)
|
|
1287
|
+
] });
|
|
1288
|
+
await client.batch(setup, "write");
|
|
1289
|
+
await verifyAuthoritySchema(client);
|
|
1290
|
+
return { schemaSha256: AUTHORITY_SCHEMA_SHA256, schemaVersion: 1, v: 1 };
|
|
1291
|
+
}
|
|
1292
|
+
async function initializeSpace(client, binding) {
|
|
1293
|
+
const purged = await queryOne(client, {
|
|
1294
|
+
sql: PURGE_ROW_SELECT,
|
|
1295
|
+
args: [binding.spaceId]
|
|
1296
|
+
});
|
|
1297
|
+
if (purged !== null)
|
|
1298
|
+
throw new OhPurgedSpaceError(parsePurgeReceiptRow(purged, binding.spaceId, binding.bindingSha256));
|
|
1299
|
+
const now = canonicalNow();
|
|
1300
|
+
try {
|
|
1301
|
+
await client.batch([
|
|
1302
|
+
{
|
|
1303
|
+
sql: `INSERT INTO oh_authority_spaces(space_id, contract_id, generation,
|
|
1304
|
+
head_operation_sha256, graph_revision_sha256, records_sha256, sequence, created_at, updated_at)
|
|
1305
|
+
SELECT ?, ?, 0, NULL, NULL, ?, 0, ?, ?
|
|
1306
|
+
WHERE NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?)
|
|
1307
|
+
ON CONFLICT(space_id) DO NOTHING`,
|
|
1308
|
+
args: [
|
|
1309
|
+
binding.spaceId,
|
|
1310
|
+
OH_CONTRACT_MANIFEST_V1.contractId,
|
|
1311
|
+
EMPTY_RECORDS_SHA2562,
|
|
1312
|
+
now,
|
|
1313
|
+
now,
|
|
1314
|
+
binding.spaceId
|
|
1315
|
+
]
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
sql: `INSERT INTO oh_authority_bindings(space_id, realm_id, profile_id, profile_kind,
|
|
1319
|
+
profile_sha256, binding_sha256, binding_json, created_at)
|
|
1320
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?
|
|
1321
|
+
WHERE NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?)
|
|
1322
|
+
AND EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
1323
|
+
ON CONFLICT(space_id) DO NOTHING`,
|
|
1324
|
+
args: [
|
|
1325
|
+
binding.spaceId,
|
|
1326
|
+
binding.realmId,
|
|
1327
|
+
binding.profile.profileId,
|
|
1328
|
+
binding.profile.profileKind,
|
|
1329
|
+
binding.profile.profileSha256,
|
|
1330
|
+
binding.bindingSha256,
|
|
1331
|
+
canonicalJson(binding),
|
|
1332
|
+
now,
|
|
1333
|
+
binding.spaceId,
|
|
1334
|
+
binding.spaceId
|
|
1335
|
+
]
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
sql: `INSERT INTO oh_authority_commit_guards(value)
|
|
1339
|
+
SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?)
|
|
1340
|
+
OR NOT EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
1341
|
+
OR NOT EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ? AND binding_sha256 = ?)`,
|
|
1342
|
+
args: [binding.spaceId, binding.spaceId, binding.spaceId, binding.bindingSha256]
|
|
1343
|
+
}
|
|
1344
|
+
], "write");
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
const raced = await queryOne(client, {
|
|
1347
|
+
sql: PURGE_ROW_SELECT,
|
|
1348
|
+
args: [binding.spaceId]
|
|
1349
|
+
});
|
|
1350
|
+
if (raced !== null)
|
|
1351
|
+
throw new OhPurgedSpaceError(parsePurgeReceiptRow(raced, binding.spaceId, binding.bindingSha256));
|
|
1352
|
+
const persisted2 = await queryOne(client, { sql: BINDING_ROW_SELECT, args: [binding.spaceId] });
|
|
1353
|
+
if (persisted2 !== null && canonicalJson(parseBindingRow(persisted2, binding.spaceId)) !== canonicalJson(binding)) {
|
|
1354
|
+
throw new OhProfileError("The remote space is already bound to a different realm or profile.");
|
|
1355
|
+
}
|
|
1356
|
+
throw error;
|
|
1357
|
+
}
|
|
1358
|
+
const persisted = await queryOne(client, { sql: BINDING_ROW_SELECT, args: [binding.spaceId] });
|
|
1359
|
+
if (persisted === null) {
|
|
1360
|
+
const raced = await queryOne(client, {
|
|
1361
|
+
sql: PURGE_ROW_SELECT,
|
|
1362
|
+
args: [binding.spaceId]
|
|
1363
|
+
});
|
|
1364
|
+
if (raced !== null)
|
|
1365
|
+
throw new OhPurgedSpaceError(parsePurgeReceiptRow(raced, binding.spaceId, binding.bindingSha256));
|
|
1366
|
+
throw new OhIntegrityError("The remote space has no persisted binding after initialization.");
|
|
1367
|
+
}
|
|
1368
|
+
if (canonicalJson(parseBindingRow(persisted, binding.spaceId)) !== canonicalJson(binding)) {
|
|
1369
|
+
throw new OhProfileError("The remote space is already bound to a different realm or profile.");
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function requireExistingSpace(client, binding) {
|
|
1373
|
+
const results = await client.batch([
|
|
1374
|
+
{ sql: BINDING_ROW_SELECT, args: [binding.spaceId] },
|
|
1375
|
+
{
|
|
1376
|
+
sql: `SELECT generation, graph_revision_sha256, head_operation_sha256,
|
|
1377
|
+
records_sha256, sequence, contract_id FROM oh_authority_spaces WHERE space_id = ?`,
|
|
1378
|
+
args: [binding.spaceId]
|
|
1379
|
+
},
|
|
1380
|
+
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] }
|
|
1381
|
+
], "read");
|
|
1382
|
+
if (results.length !== 3) {
|
|
1383
|
+
throw new OhIntegrityError("The remote authority returned an incomplete existing-space proof.");
|
|
1384
|
+
}
|
|
1385
|
+
const [bindingResult, spaceResult, purgeResult] = results;
|
|
1386
|
+
const purgeRow = purgeResult.rows[0];
|
|
1387
|
+
if (purgeRow !== undefined) {
|
|
1388
|
+
throw new OhPurgedSpaceError(parsePurgeReceiptRow(purgeRow, binding.spaceId, binding.bindingSha256));
|
|
1389
|
+
}
|
|
1390
|
+
const bindingRow = bindingResult.rows[0];
|
|
1391
|
+
const spaceRow = spaceResult.rows[0];
|
|
1392
|
+
if (bindingRow === undefined || spaceRow === undefined) {
|
|
1393
|
+
throw new OhIntegrityError("The requested remote Oh space does not already exist.");
|
|
1394
|
+
}
|
|
1395
|
+
const persisted = parseBindingRow(bindingRow, binding.spaceId);
|
|
1396
|
+
if (canonicalJson(persisted) !== canonicalJson(binding)) {
|
|
1397
|
+
throw new OhProfileError("The remote space is bound to a different realm or profile.");
|
|
1398
|
+
}
|
|
1399
|
+
if (rowValue(spaceRow, "contract_id", 5) !== OH_CONTRACT_MANIFEST_V1.contractId) {
|
|
1400
|
+
throw new OhIntegrityError("The existing remote space uses a different Oh contract.");
|
|
1401
|
+
}
|
|
1402
|
+
parseHeadRow(spaceRow);
|
|
1403
|
+
}
|
|
1404
|
+
var PURGE_PAYLOAD_TABLES = [
|
|
1405
|
+
"oh_authority_spaces",
|
|
1406
|
+
"oh_authority_bindings",
|
|
1407
|
+
"oh_authority_operations",
|
|
1408
|
+
"oh_authority_operation_records",
|
|
1409
|
+
"oh_authority_records",
|
|
1410
|
+
"oh_authority_dependencies"
|
|
1411
|
+
];
|
|
1412
|
+
async function assertRemotePurgeComplete(client, binding, expected) {
|
|
1413
|
+
const results = await client.batch([
|
|
1414
|
+
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] },
|
|
1415
|
+
...PURGE_PAYLOAD_TABLES.map((table) => ({
|
|
1416
|
+
sql: `SELECT count(*) AS count FROM ${table} WHERE space_id = ?`,
|
|
1417
|
+
args: [binding.spaceId]
|
|
1418
|
+
})),
|
|
1419
|
+
{ sql: `SELECT count(*) AS count FROM oh_authority_operation_records AS materialized
|
|
1420
|
+
LEFT JOIN oh_authority_operations AS operation
|
|
1421
|
+
ON operation.operation_sha256 = materialized.operation_sha256
|
|
1422
|
+
WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id` }
|
|
1423
|
+
], "read");
|
|
1424
|
+
const receiptRow = results[0]?.rows[0];
|
|
1425
|
+
if (receiptRow === undefined || canonicalJson(parsePurgeReceiptRow(receiptRow, binding.spaceId, binding.bindingSha256)) !== canonicalJson(expected)) {
|
|
1426
|
+
throw new OhIntegrityError("The remote purge receipt differs from the requested purge.");
|
|
1427
|
+
}
|
|
1428
|
+
for (let index = 0;index < PURGE_PAYLOAD_TABLES.length; index += 1) {
|
|
1429
|
+
const countRow = results[index + 1]?.rows[0];
|
|
1430
|
+
if (countRow === undefined || integer(rowValue(countRow, "count", 0)) !== 0) {
|
|
1431
|
+
throw new OhIntegrityError(`Remote purge left rows in ${PURGE_PAYLOAD_TABLES[index]}.`);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const orphanRow = results[PURGE_PAYLOAD_TABLES.length + 1]?.rows[0];
|
|
1435
|
+
if (orphanRow === undefined || integer(rowValue(orphanRow, "count", 0)) !== 0) {
|
|
1436
|
+
throw new OhIntegrityError("Remote purge left an orphaned or cross-space operation record.");
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
class OhLibSqlStoreV1 {
|
|
1441
|
+
binding;
|
|
1442
|
+
#client;
|
|
1443
|
+
#closeClient;
|
|
1444
|
+
#closed = false;
|
|
1445
|
+
#purged = null;
|
|
1446
|
+
constructor(client, binding, closeClient) {
|
|
1447
|
+
this.#client = client;
|
|
1448
|
+
this.binding = binding;
|
|
1449
|
+
this.#closeClient = closeClient;
|
|
1450
|
+
}
|
|
1451
|
+
#assertOpen() {
|
|
1452
|
+
if (this.#purged !== null)
|
|
1453
|
+
throw new OhPurgedSpaceError(this.#purged);
|
|
1454
|
+
if (this.#closed)
|
|
1455
|
+
throw new Error("The Oh libSQL store is closed.");
|
|
1456
|
+
}
|
|
1457
|
+
async head() {
|
|
1458
|
+
this.#assertOpen();
|
|
1459
|
+
const row = await queryOne(this.#client, {
|
|
1460
|
+
sql: `SELECT generation, graph_revision_sha256,
|
|
1461
|
+
head_operation_sha256, records_sha256, sequence FROM oh_authority_spaces WHERE space_id = ?`,
|
|
1462
|
+
args: [this.binding.spaceId]
|
|
1463
|
+
});
|
|
1464
|
+
if (row === null) {
|
|
1465
|
+
const purged = await this.#readPurge();
|
|
1466
|
+
if (purged !== null) {
|
|
1467
|
+
this.#purged = purged;
|
|
1468
|
+
throw new OhPurgedSpaceError(purged);
|
|
1469
|
+
}
|
|
1470
|
+
throw new OhIntegrityError("The remote Oh space does not exist.");
|
|
1471
|
+
}
|
|
1472
|
+
return parseHeadRow(row);
|
|
1473
|
+
}
|
|
1474
|
+
async#readPurge() {
|
|
1475
|
+
const row = await queryOne(this.#client, {
|
|
1476
|
+
sql: PURGE_ROW_SELECT,
|
|
1477
|
+
args: [this.binding.spaceId]
|
|
1478
|
+
});
|
|
1479
|
+
if (row === null)
|
|
1480
|
+
return null;
|
|
1481
|
+
return parsePurgeReceiptRow(row, this.binding.spaceId, this.binding.bindingSha256);
|
|
1482
|
+
}
|
|
1483
|
+
async#headAt(reference) {
|
|
1484
|
+
const parsed = parseOhHeadRefV1(reference);
|
|
1485
|
+
if (parsed === null)
|
|
1486
|
+
throw new TypeError("Invalid Oh head reference.");
|
|
1487
|
+
if (parsed.sequence === 0)
|
|
1488
|
+
return emptyOhHeadV1();
|
|
1489
|
+
const row = await queryOne(this.#client, { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
1490
|
+
WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, parsed.sequence] });
|
|
1491
|
+
if (row === null)
|
|
1492
|
+
throw new OhConflictError("The requested head is not present in this space.");
|
|
1493
|
+
const operation = parseOperationRow(row, { spaceId: this.binding.spaceId });
|
|
1494
|
+
if (operation.spaceId !== this.binding.spaceId || operation.sequence !== parsed.sequence || operation.operationSha256 !== parsed.operationSha256) {
|
|
1495
|
+
throw new OhConflictError("The requested sequence identifies a different operation head.");
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
generation: operation.sequence,
|
|
1499
|
+
graphRevisionSha256: operation.graphRevisionSha256,
|
|
1500
|
+
operationSha256: operation.operationSha256,
|
|
1501
|
+
recordsSha256: operation.recordsSha256,
|
|
1502
|
+
sequence: operation.sequence,
|
|
1503
|
+
v: 1
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async#currentMaterializedSnapshot(expectedHead, maximumRecords) {
|
|
1507
|
+
const provenancePredicate = `operation.space_id = ? AND (operation.operation_sha256 IS ?
|
|
1508
|
+
OR operation.operation_sha256 IN (SELECT record.operation_sha256
|
|
1509
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?))`;
|
|
1510
|
+
const results = await this.#client.batch([
|
|
1511
|
+
{ sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
1512
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] },
|
|
1513
|
+
{
|
|
1514
|
+
sql: `SELECT
|
|
1515
|
+
(SELECT count(*) FROM (SELECT DISTINCT operation.operation_sha256
|
|
1516
|
+
FROM oh_authority_operations AS operation WHERE ${provenancePredicate})) AS provenance_count,
|
|
1517
|
+
(SELECT coalesce(sum(bytes), 0) FROM (SELECT DISTINCT operation.operation_sha256,
|
|
1518
|
+
${OPERATION_RESPONSE_BYTES} AS bytes FROM oh_authority_operations AS operation
|
|
1519
|
+
WHERE ${provenancePredicate})) AS provenance_bytes,
|
|
1520
|
+
(SELECT count(*) FROM oh_authority_records WHERE space_id = ?) AS record_count,
|
|
1521
|
+
(SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) FROM oh_authority_records AS record
|
|
1522
|
+
WHERE record.space_id = ?) AS record_bytes,
|
|
1523
|
+
(SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0)
|
|
1524
|
+
FROM oh_authority_dependencies AS dependency
|
|
1525
|
+
WHERE dependency.space_id = ?) AS dependency_bytes`,
|
|
1526
|
+
args: [
|
|
1527
|
+
this.binding.spaceId,
|
|
1528
|
+
expectedHead.operationSha256,
|
|
1529
|
+
this.binding.spaceId,
|
|
1530
|
+
this.binding.spaceId,
|
|
1531
|
+
expectedHead.operationSha256,
|
|
1532
|
+
this.binding.spaceId,
|
|
1533
|
+
this.binding.spaceId,
|
|
1534
|
+
this.binding.spaceId,
|
|
1535
|
+
this.binding.spaceId
|
|
1536
|
+
]
|
|
1537
|
+
},
|
|
1538
|
+
{ sql: `SELECT record_key, kind, record_sha256, record_json, operation_sha256, sequence
|
|
1539
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?
|
|
1540
|
+
AND (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0)
|
|
1541
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?) <= ? ORDER BY record_key`, args: [
|
|
1542
|
+
this.binding.spaceId,
|
|
1543
|
+
this.binding.spaceId,
|
|
1544
|
+
OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes
|
|
1545
|
+
] },
|
|
1546
|
+
{
|
|
1547
|
+
sql: `SELECT record_key, dependency_key FROM oh_authority_dependencies
|
|
1548
|
+
AS dependency WHERE dependency.space_id = ?
|
|
1549
|
+
AND (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0)
|
|
1550
|
+
FROM oh_authority_dependencies AS dependency WHERE dependency.space_id = ?) <= ?
|
|
1551
|
+
ORDER BY record_key, dependency_key`,
|
|
1552
|
+
args: [this.binding.spaceId, this.binding.spaceId, OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes]
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS}
|
|
1556
|
+
FROM oh_authority_operations AS operation
|
|
1557
|
+
WHERE ${provenancePredicate}
|
|
1558
|
+
AND (SELECT coalesce(sum(bytes), 0) FROM (SELECT DISTINCT candidate.operation_sha256,
|
|
1559
|
+
${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS bytes
|
|
1560
|
+
FROM oh_authority_operations AS candidate
|
|
1561
|
+
WHERE candidate.space_id = ? AND (candidate.operation_sha256 IS ?
|
|
1562
|
+
OR candidate.operation_sha256 IN (SELECT record.operation_sha256
|
|
1563
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?)))) <= ?
|
|
1564
|
+
ORDER BY operation.sequence`,
|
|
1565
|
+
args: [
|
|
1566
|
+
this.binding.spaceId,
|
|
1567
|
+
expectedHead.operationSha256,
|
|
1568
|
+
this.binding.spaceId,
|
|
1569
|
+
this.binding.spaceId,
|
|
1570
|
+
expectedHead.operationSha256,
|
|
1571
|
+
this.binding.spaceId,
|
|
1572
|
+
OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes
|
|
1573
|
+
]
|
|
1574
|
+
},
|
|
1575
|
+
{ sql: `SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum
|
|
1576
|
+
FROM oh_authority_operations WHERE space_id = ?`, args: [this.binding.spaceId] }
|
|
1577
|
+
], "read");
|
|
1578
|
+
if (results.length !== 6)
|
|
1579
|
+
throw new OhIntegrityError("The remote authority returned an incomplete snapshot batch.");
|
|
1580
|
+
const [headResult, sizeResult, recordResult, dependencyResult, provenanceResult, historyResult] = results;
|
|
1581
|
+
const headRow = headResult.rows[0];
|
|
1582
|
+
if (headRow === undefined) {
|
|
1583
|
+
const purge = await this.#readPurge();
|
|
1584
|
+
if (purge !== null) {
|
|
1585
|
+
this.#purged = purge;
|
|
1586
|
+
throw new OhPurgedSpaceError(purge);
|
|
1587
|
+
}
|
|
1588
|
+
throw new OhIntegrityError("The remote Oh space disappeared while reading its snapshot.");
|
|
1589
|
+
}
|
|
1590
|
+
const head = parseHeadRow(headRow);
|
|
1591
|
+
if (canonicalJson(head) !== canonicalJson(expectedHead)) {
|
|
1592
|
+
throw new OhConflictError("The remote space head changed while reading its current snapshot.");
|
|
1593
|
+
}
|
|
1594
|
+
const size = sizeResult.rows[0];
|
|
1595
|
+
const provenanceOperations = size === undefined ? null : integer(rowValue(size, "provenance_count", 0));
|
|
1596
|
+
const provenanceBytes = size === undefined ? null : integer(rowValue(size, "provenance_bytes", 1));
|
|
1597
|
+
const recordCount = size === undefined ? null : integer(rowValue(size, "record_count", 2));
|
|
1598
|
+
const recordBytes = size === undefined ? null : integer(rowValue(size, "record_bytes", 3));
|
|
1599
|
+
const dependencyBytes = size === undefined ? null : integer(rowValue(size, "dependency_bytes", 4));
|
|
1600
|
+
if (provenanceOperations === null || provenanceBytes === null || recordCount === null || recordBytes === null || dependencyBytes === null || provenanceOperations > OH_LIBSQL_STORE_LIMITS_V1.historyOperations || provenanceBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes || recordBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes || dependencyBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes) {
|
|
1601
|
+
throw new RangeError("The current libSQL materialization exceeds its provider-safe response bounds.");
|
|
1602
|
+
}
|
|
1603
|
+
if (recordResult.rows.length !== recordCount || provenanceResult.rows.length !== provenanceOperations) {
|
|
1604
|
+
throw new OhIntegrityError("The provider-safe snapshot queries omitted bounded authority rows.");
|
|
1605
|
+
}
|
|
1606
|
+
const history = historyResult.rows[0];
|
|
1607
|
+
const operationCount = history === undefined ? null : integer(rowValue(history, "count", 0));
|
|
1608
|
+
const minimumValue = history === undefined ? undefined : rowValue(history, "minimum", 1);
|
|
1609
|
+
const maximumValue = history === undefined ? undefined : rowValue(history, "maximum", 2);
|
|
1610
|
+
const minimumSequence = history === undefined ? null : integer(rowValue(history, "minimum", 1));
|
|
1611
|
+
const maximumSequence = history === undefined ? null : integer(rowValue(history, "maximum", 2));
|
|
1612
|
+
if (operationCount !== head.sequence || head.sequence === 0 && (minimumValue !== null || maximumValue !== null) || head.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== head.sequence)) {
|
|
1613
|
+
throw new OhIntegrityError("The remote operation history does not exactly cover its current head.");
|
|
1614
|
+
}
|
|
1615
|
+
if (recordResult.rows.length > maximumRecords) {
|
|
1616
|
+
throw new RangeError("The remote graph exceeds the requested record snapshot bound.");
|
|
1617
|
+
}
|
|
1618
|
+
const provenanceBySha256 = new Map;
|
|
1619
|
+
for (const row of provenanceResult.rows) {
|
|
1620
|
+
const operation = parseOperationRow(row, { spaceId: this.binding.spaceId });
|
|
1621
|
+
if (provenanceBySha256.has(operation.operationSha256)) {
|
|
1622
|
+
throw new OhIntegrityError("A current materialization provenance operation is invalid.");
|
|
1623
|
+
}
|
|
1624
|
+
provenanceBySha256.set(operation.operationSha256, operation);
|
|
1625
|
+
}
|
|
1626
|
+
if (provenanceBySha256.size !== provenanceOperations || head.operationSha256 !== null && !provenanceBySha256.has(head.operationSha256)) {
|
|
1627
|
+
throw new OhIntegrityError("The current materialization omitted required provenance operations.");
|
|
1628
|
+
}
|
|
1629
|
+
if (head.sequence > 0) {
|
|
1630
|
+
const terminal = head.operationSha256 === null ? undefined : provenanceBySha256.get(head.operationSha256);
|
|
1631
|
+
if (terminal === undefined || terminal.sequence !== head.sequence || terminal.graphRevisionSha256 !== head.graphRevisionSha256 || terminal.recordsSha256 !== head.recordsSha256) {
|
|
1632
|
+
throw new OhIntegrityError("The remote space head differs from its terminal canonical operation.");
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const materialized = recordResult.rows.map((row) => {
|
|
1636
|
+
const json = rowValue(row, "record_json", 3);
|
|
1637
|
+
if (typeof json !== "string")
|
|
1638
|
+
throw new OhIntegrityError("A materialized remote record is not JSON text.");
|
|
1639
|
+
let value;
|
|
1640
|
+
try {
|
|
1641
|
+
value = JSON.parse(json);
|
|
1642
|
+
} catch {
|
|
1643
|
+
throw new OhIntegrityError("A materialized remote record is invalid.");
|
|
1644
|
+
}
|
|
1645
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
1646
|
+
const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 4));
|
|
1647
|
+
const sequence = integer(rowValue(row, "sequence", 5));
|
|
1648
|
+
if (record === null || canonicalJson(record) !== json || operationSha256 === null || sequence === null || sequence < 1 || sequence > head.sequence || rowValue(row, "record_key", 0) !== record.key || rowValue(row, "kind", 1) !== record.kind || rowValue(row, "record_sha256", 2) !== record.recordSha256) {
|
|
1649
|
+
throw new OhIntegrityError("A materialized remote record is invalid.");
|
|
1650
|
+
}
|
|
1651
|
+
const provenance = provenanceBySha256.get(operationSha256);
|
|
1652
|
+
if (provenance === undefined || provenance.sequence !== sequence || !provenance.changes.some((change) => change.kind === "put" && canonicalJson(change.record) === json)) {
|
|
1653
|
+
throw new OhIntegrityError("A materialized remote record has no exact canonical provenance put.");
|
|
1654
|
+
}
|
|
1655
|
+
return { record, sequence };
|
|
1656
|
+
});
|
|
1657
|
+
const records = materialized.map(({ record }) => record);
|
|
1658
|
+
if (canonicalSha256(records.map(knowledgeGraphRecordRefV1)) !== head.recordsSha256) {
|
|
1659
|
+
throw new OhIntegrityError("Materialized remote records do not reproduce the current head.");
|
|
1660
|
+
}
|
|
1661
|
+
const dependencyRows = dependencyResult.rows.map((row) => ({
|
|
1662
|
+
dependency_key: rowValue(row, "dependency_key", 1),
|
|
1663
|
+
record_key: rowValue(row, "record_key", 0)
|
|
1664
|
+
}));
|
|
1665
|
+
const expectedDependencies = records.flatMap((record) => record.dependencies.map((dependency) => ({ dependency_key: dependency, record_key: record.key })));
|
|
1666
|
+
if (canonicalJson(dependencyRows) !== canonicalJson(expectedDependencies)) {
|
|
1667
|
+
throw new OhIntegrityError("Materialized remote dependencies do not match their record envelopes.");
|
|
1668
|
+
}
|
|
1669
|
+
return { head, records, v: 1 };
|
|
1670
|
+
}
|
|
1671
|
+
async snapshot(options = {}) {
|
|
1672
|
+
this.#assertOpen();
|
|
1673
|
+
const current = await this.head();
|
|
1674
|
+
const target = options.head === undefined ? current : await this.#headAt(options.head);
|
|
1675
|
+
if (target.sequence > current.sequence)
|
|
1676
|
+
throw new OhConflictError("The requested head is ahead of this space.");
|
|
1677
|
+
const maximumRecords = options.maximumRecords ?? OH_GRAPH_LIMITS_V1.recordsPerSnapshot;
|
|
1678
|
+
if (!Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
1679
|
+
throw new RangeError(`maximumRecords must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`);
|
|
1680
|
+
}
|
|
1681
|
+
if (target.operationSha256 === current.operationSha256) {
|
|
1682
|
+
return await this.#currentMaterializedSnapshot(current, maximumRecords);
|
|
1683
|
+
}
|
|
1684
|
+
if (target.sequence > OH_LIBSQL_STORE_LIMITS_V1.historyOperations) {
|
|
1685
|
+
throw new RangeError("The requested libSQL history exceeds its operation replay bound.");
|
|
1686
|
+
}
|
|
1687
|
+
const historyResults = await this.#client.batch([
|
|
1688
|
+
{
|
|
1689
|
+
sql: `SELECT count(*) AS count, min(operation.sequence) AS minimum,
|
|
1690
|
+
max(operation.sequence) AS maximum,
|
|
1691
|
+
coalesce(sum(length(CAST(operation.operation_json AS BLOB))), 0) AS canonical_bytes,
|
|
1692
|
+
coalesce(sum(${OPERATION_RESPONSE_BYTES}), 0) AS response_bytes
|
|
1693
|
+
FROM oh_authority_operations AS operation
|
|
1694
|
+
WHERE operation.space_id = ? AND operation.sequence <= ?`,
|
|
1695
|
+
args: [this.binding.spaceId, target.sequence]
|
|
1696
|
+
},
|
|
1697
|
+
{ sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations AS operation
|
|
1698
|
+
WHERE operation.space_id = ? AND operation.sequence <= ?
|
|
1699
|
+
AND (SELECT coalesce(sum(length(CAST(candidate.operation_json AS BLOB))), 0)
|
|
1700
|
+
FROM oh_authority_operations AS candidate
|
|
1701
|
+
WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ?
|
|
1702
|
+
AND (SELECT coalesce(sum(${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")}), 0)
|
|
1703
|
+
FROM oh_authority_operations AS candidate
|
|
1704
|
+
WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ?
|
|
1705
|
+
ORDER BY operation.sequence`, args: [
|
|
1706
|
+
this.binding.spaceId,
|
|
1707
|
+
target.sequence,
|
|
1708
|
+
this.binding.spaceId,
|
|
1709
|
+
target.sequence,
|
|
1710
|
+
OH_LIBSQL_STORE_LIMITS_V1.historyBytes,
|
|
1711
|
+
this.binding.spaceId,
|
|
1712
|
+
target.sequence,
|
|
1713
|
+
OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes
|
|
1714
|
+
] },
|
|
1715
|
+
{ sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] }
|
|
1716
|
+
], "read");
|
|
1717
|
+
if (historyResults.length !== 3) {
|
|
1718
|
+
throw new OhIntegrityError("The remote authority returned an incomplete history batch.");
|
|
1719
|
+
}
|
|
1720
|
+
const [historySizeResult, historyRowResult, purgeResult] = historyResults;
|
|
1721
|
+
const historySizeRow = historySizeResult.rows[0];
|
|
1722
|
+
const historyBytes = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "canonical_bytes", 3));
|
|
1723
|
+
const responseBytes = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "response_bytes", 4));
|
|
1724
|
+
if (historyBytes === null || historyBytes > OH_LIBSQL_STORE_LIMITS_V1.historyBytes || responseBytes === null || responseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes) {
|
|
1725
|
+
throw new RangeError("The requested libSQL history exceeds its provider-safe replay bounds.");
|
|
1726
|
+
}
|
|
1727
|
+
const historyCount = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "count", 0));
|
|
1728
|
+
const minimumSequence = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "minimum", 1));
|
|
1729
|
+
const maximumSequence = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "maximum", 2));
|
|
1730
|
+
if (historyCount !== target.sequence || target.sequence === 0 && (rowValue(historySizeRow, "minimum", 1) !== null || rowValue(historySizeRow, "maximum", 2) !== null) || target.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== target.sequence) || historyRowResult.rows.length !== historyCount) {
|
|
1731
|
+
const purgeRow = purgeResult.rows[0];
|
|
1732
|
+
if (purgeRow !== undefined) {
|
|
1733
|
+
const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256);
|
|
1734
|
+
this.#purged = purge;
|
|
1735
|
+
throw new OhPurgedSpaceError(purge);
|
|
1736
|
+
}
|
|
1737
|
+
throw new OhIntegrityError("The remote operation history does not exactly cover the requested head.");
|
|
1738
|
+
}
|
|
1739
|
+
const operations = historyRowResult.rows.map((row) => parseOperationRow(row, { spaceId: this.binding.spaceId }));
|
|
1740
|
+
const snapshot = replayOhOperationsV1(this.binding.spaceId, operations, maximumRecords);
|
|
1741
|
+
if (snapshot.head.operationSha256 !== target.operationSha256 || snapshot.head.recordsSha256 !== target.recordsSha256) {
|
|
1742
|
+
throw new OhIntegrityError("Remote operation replay does not reproduce the requested head.");
|
|
1743
|
+
}
|
|
1744
|
+
return snapshot;
|
|
1745
|
+
}
|
|
1746
|
+
async changesSince(fromValue, options = {}) {
|
|
1747
|
+
this.#assertOpen();
|
|
1748
|
+
const from = parseOhHeadRefV1(fromValue);
|
|
1749
|
+
if (from === null)
|
|
1750
|
+
throw new TypeError("Invalid change-feed cursor.");
|
|
1751
|
+
const requestedThrough = options.through === undefined ? undefined : parseOhHeadRefV1(options.through);
|
|
1752
|
+
if (requestedThrough === null)
|
|
1753
|
+
throw new TypeError("Invalid change-feed through head.");
|
|
1754
|
+
const limit = normalizeLimit(options.limit, OH_LIBSQL_STORE_LIMITS_V1.changeFeedLimit, OH_LIBSQL_STORE_LIMITS_V1.changeFeedLimit);
|
|
1755
|
+
const throughSequence = requestedThrough?.sequence ?? null;
|
|
1756
|
+
const results = await this.#client.batch([
|
|
1757
|
+
{ sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
1758
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] },
|
|
1759
|
+
{ sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
1760
|
+
WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, from.sequence] },
|
|
1761
|
+
{ sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
1762
|
+
WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, throughSequence] },
|
|
1763
|
+
{ sql: `SELECT count(*) AS count, coalesce(sum(response_bytes), 0) AS response_bytes FROM (
|
|
1764
|
+
SELECT ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS response_bytes
|
|
1765
|
+
FROM oh_authority_operations AS candidate
|
|
1766
|
+
WHERE candidate.space_id = ? AND candidate.sequence > ?
|
|
1767
|
+
AND candidate.sequence <= coalesce(?,
|
|
1768
|
+
(SELECT sequence FROM oh_authority_spaces WHERE space_id = ?))
|
|
1769
|
+
ORDER BY candidate.sequence LIMIT ?
|
|
1770
|
+
)`, args: [
|
|
1771
|
+
this.binding.spaceId,
|
|
1772
|
+
from.sequence,
|
|
1773
|
+
throughSequence,
|
|
1774
|
+
this.binding.spaceId,
|
|
1775
|
+
limit + 1
|
|
1776
|
+
] },
|
|
1777
|
+
{
|
|
1778
|
+
sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
1779
|
+
AS operation WHERE operation.space_id = ? AND operation.sequence > ?
|
|
1780
|
+
AND operation.sequence <= coalesce(?,
|
|
1781
|
+
(SELECT sequence FROM oh_authority_spaces WHERE space_id = ?))
|
|
1782
|
+
AND (SELECT coalesce(sum(response_bytes), 0) FROM (
|
|
1783
|
+
SELECT ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS response_bytes
|
|
1784
|
+
FROM oh_authority_operations AS candidate
|
|
1785
|
+
WHERE candidate.space_id = ? AND candidate.sequence > ?
|
|
1786
|
+
AND candidate.sequence <= coalesce(?,
|
|
1787
|
+
(SELECT sequence FROM oh_authority_spaces WHERE space_id = ?))
|
|
1788
|
+
ORDER BY candidate.sequence LIMIT ?
|
|
1789
|
+
)) <= ?
|
|
1790
|
+
ORDER BY operation.sequence LIMIT ?`,
|
|
1791
|
+
args: [
|
|
1792
|
+
this.binding.spaceId,
|
|
1793
|
+
from.sequence,
|
|
1794
|
+
throughSequence,
|
|
1795
|
+
this.binding.spaceId,
|
|
1796
|
+
this.binding.spaceId,
|
|
1797
|
+
from.sequence,
|
|
1798
|
+
throughSequence,
|
|
1799
|
+
this.binding.spaceId,
|
|
1800
|
+
limit + 1,
|
|
1801
|
+
OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes,
|
|
1802
|
+
limit + 1
|
|
1803
|
+
]
|
|
1804
|
+
},
|
|
1805
|
+
{ sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] }
|
|
1806
|
+
], "read");
|
|
1807
|
+
if (results.length !== 6)
|
|
1808
|
+
throw new OhIntegrityError("The remote authority returned an incomplete change-feed batch.");
|
|
1809
|
+
const [currentResult, fromResult, throughResult, pageSizeResult, pageResult, purgeResult] = results;
|
|
1810
|
+
const currentRow = currentResult.rows[0];
|
|
1811
|
+
if (currentRow === undefined) {
|
|
1812
|
+
const purgeRow = purgeResult.rows[0];
|
|
1813
|
+
if (purgeRow !== undefined) {
|
|
1814
|
+
const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256);
|
|
1815
|
+
this.#purged = purge;
|
|
1816
|
+
throw new OhPurgedSpaceError(purge);
|
|
1817
|
+
}
|
|
1818
|
+
throw new OhIntegrityError("The remote Oh space disappeared while reading its change feed.");
|
|
1819
|
+
}
|
|
1820
|
+
const current = parseHeadRow(currentRow);
|
|
1821
|
+
const resolveHead = (reference, result) => {
|
|
1822
|
+
if (reference.sequence === 0)
|
|
1823
|
+
return emptyOhHeadV1();
|
|
1824
|
+
const row = result.rows[0];
|
|
1825
|
+
if (row === undefined)
|
|
1826
|
+
throw new OhConflictError("A requested change-feed head is not present in this space.");
|
|
1827
|
+
const operation = parseOperationRow(row, { spaceId: this.binding.spaceId });
|
|
1828
|
+
if (operation.spaceId !== this.binding.spaceId || operation.sequence !== reference.sequence || operation.operationSha256 !== reference.operationSha256) {
|
|
1829
|
+
throw new OhConflictError("A requested change-feed sequence identifies a different operation head.");
|
|
1830
|
+
}
|
|
1831
|
+
return {
|
|
1832
|
+
generation: operation.sequence,
|
|
1833
|
+
graphRevisionSha256: operation.graphRevisionSha256,
|
|
1834
|
+
operationSha256: operation.operationSha256,
|
|
1835
|
+
recordsSha256: operation.recordsSha256,
|
|
1836
|
+
sequence: operation.sequence,
|
|
1837
|
+
v: 1
|
|
1838
|
+
};
|
|
1839
|
+
};
|
|
1840
|
+
const fromHead = resolveHead(from, fromResult);
|
|
1841
|
+
const through = requestedThrough === undefined ? current : resolveHead(requestedThrough, throughResult);
|
|
1842
|
+
if (fromHead.sequence > through.sequence || through.sequence > current.sequence) {
|
|
1843
|
+
throw new OhConflictError("The change-feed bounds do not identify one remote history prefix.");
|
|
1844
|
+
}
|
|
1845
|
+
const pageSizeRow = pageSizeResult.rows[0];
|
|
1846
|
+
const pageCount = pageSizeRow === undefined ? null : integer(rowValue(pageSizeRow, "count", 0));
|
|
1847
|
+
const pageResponseBytes = pageSizeRow === undefined ? null : integer(rowValue(pageSizeRow, "response_bytes", 1));
|
|
1848
|
+
if (pageCount === null || pageCount > limit + 1 || pageResponseBytes === null) {
|
|
1849
|
+
throw new OhIntegrityError("The remote change feed returned invalid response bounds.");
|
|
1850
|
+
}
|
|
1851
|
+
if (pageResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes) {
|
|
1852
|
+
throw new RangeError("The requested change-feed page exceeds its provider response bound.");
|
|
1853
|
+
}
|
|
1854
|
+
const parsed = pageResult.rows.map((row) => parseOperationRow(row, { spaceId: this.binding.spaceId }));
|
|
1855
|
+
if (parsed.length !== pageCount) {
|
|
1856
|
+
throw new OhIntegrityError("The remote change feed omitted provider-bounded rows.");
|
|
1857
|
+
}
|
|
1858
|
+
const hasMore = parsed.length > limit;
|
|
1859
|
+
const operations = parsed.slice(0, limit);
|
|
1860
|
+
let prior = fromHead;
|
|
1861
|
+
for (const operation of parsed) {
|
|
1862
|
+
if (operation.spaceId !== this.binding.spaceId || operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) {
|
|
1863
|
+
throw new OhIntegrityError("The remote change feed contains a gap or fork.");
|
|
1864
|
+
}
|
|
1865
|
+
prior = { operationSha256: operation.operationSha256, sequence: operation.sequence };
|
|
1866
|
+
}
|
|
1867
|
+
if (!hasMore && (prior.sequence !== through.sequence || prior.operationSha256 !== through.operationSha256)) {
|
|
1868
|
+
throw new OhIntegrityError("The remote change feed does not reach its pinned through head.");
|
|
1869
|
+
}
|
|
1870
|
+
const last = operations.at(-1);
|
|
1871
|
+
const to = last === undefined ? { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence } : { operationSha256: last.operationSha256, sequence: last.sequence };
|
|
1872
|
+
return {
|
|
1873
|
+
from: { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence },
|
|
1874
|
+
hasMore,
|
|
1875
|
+
operations,
|
|
1876
|
+
through,
|
|
1877
|
+
to,
|
|
1878
|
+
v: 1
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
async#assertMaterializedSnapshot(snapshot) {
|
|
1882
|
+
if (snapshot.head.sequence > OH_LIBSQL_STORE_LIMITS_V1.historyOperations) {
|
|
1883
|
+
throw new RangeError("The libSQL authority exceeds its explicit verification operation bound.");
|
|
1884
|
+
}
|
|
1885
|
+
const verificationResults = await this.#client.batch([
|
|
1886
|
+
{ sql: `SELECT * FROM (WITH bounded_operation AS (
|
|
1887
|
+
SELECT * FROM oh_authority_operations
|
|
1888
|
+
WHERE space_id = ? AND sequence <= ?
|
|
1889
|
+
) SELECT count(*) AS operation_count, min(operation.sequence) AS minimum,
|
|
1890
|
+
max(operation.sequence) AS maximum,
|
|
1891
|
+
coalesce(sum(length(CAST(operation.operation_json AS BLOB))), 0) AS canonical_bytes,
|
|
1892
|
+
coalesce(sum(${OPERATION_RESPONSE_BYTES}), 0) AS operation_response_bytes,
|
|
1893
|
+
(SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0)
|
|
1894
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?) AS record_response_bytes,
|
|
1895
|
+
(SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0)
|
|
1896
|
+
FROM oh_authority_dependencies AS dependency
|
|
1897
|
+
WHERE dependency.space_id = ?) AS dependency_response_bytes,
|
|
1898
|
+
(SELECT coalesce(sum(${OPERATION_RECORD_RESPONSE_BYTES}), 0)
|
|
1899
|
+
FROM oh_authority_operation_records AS materialized
|
|
1900
|
+
JOIN bounded_operation AS owner
|
|
1901
|
+
ON owner.operation_sha256 = materialized.operation_sha256) AS operation_record_response_bytes
|
|
1902
|
+
FROM bounded_operation AS operation)`, args: [
|
|
1903
|
+
this.binding.spaceId,
|
|
1904
|
+
snapshot.head.sequence,
|
|
1905
|
+
this.binding.spaceId,
|
|
1906
|
+
this.binding.spaceId
|
|
1907
|
+
] },
|
|
1908
|
+
{ sql: `SELECT ${OPERATION_ROW_COLUMNS}
|
|
1909
|
+
FROM oh_authority_operations AS operation
|
|
1910
|
+
WHERE operation.space_id = ? AND operation.sequence <= ?
|
|
1911
|
+
AND (SELECT coalesce(sum(length(CAST(candidate.operation_json AS BLOB))), 0)
|
|
1912
|
+
FROM oh_authority_operations AS candidate
|
|
1913
|
+
WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ?
|
|
1914
|
+
AND (SELECT coalesce(sum(${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")}), 0)
|
|
1915
|
+
FROM oh_authority_operations AS candidate
|
|
1916
|
+
WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ?
|
|
1917
|
+
ORDER BY operation.sequence`, args: [
|
|
1918
|
+
this.binding.spaceId,
|
|
1919
|
+
snapshot.head.sequence,
|
|
1920
|
+
this.binding.spaceId,
|
|
1921
|
+
snapshot.head.sequence,
|
|
1922
|
+
OH_LIBSQL_STORE_LIMITS_V1.historyBytes,
|
|
1923
|
+
this.binding.spaceId,
|
|
1924
|
+
snapshot.head.sequence,
|
|
1925
|
+
OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes
|
|
1926
|
+
] },
|
|
1927
|
+
{ sql: `SELECT record_key, kind, record_sha256, record_json, operation_sha256, sequence
|
|
1928
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?
|
|
1929
|
+
AND (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0)
|
|
1930
|
+
FROM oh_authority_records AS record WHERE record.space_id = ?) <= ?
|
|
1931
|
+
ORDER BY record.record_key`, args: [
|
|
1932
|
+
this.binding.spaceId,
|
|
1933
|
+
this.binding.spaceId,
|
|
1934
|
+
OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes
|
|
1935
|
+
] },
|
|
1936
|
+
{ sql: `SELECT record_key, dependency_key FROM oh_authority_dependencies AS dependency
|
|
1937
|
+
WHERE dependency.space_id = ?
|
|
1938
|
+
AND (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0)
|
|
1939
|
+
FROM oh_authority_dependencies AS dependency WHERE dependency.space_id = ?) <= ?
|
|
1940
|
+
ORDER BY dependency.record_key, dependency.dependency_key`, args: [
|
|
1941
|
+
this.binding.spaceId,
|
|
1942
|
+
this.binding.spaceId,
|
|
1943
|
+
OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes
|
|
1944
|
+
] },
|
|
1945
|
+
{ sql: `SELECT materialized.space_id, materialized.operation_sha256, materialized.ordinal,
|
|
1946
|
+
materialized.record_key, materialized.change_kind, materialized.record_sha256
|
|
1947
|
+
FROM oh_authority_operation_records AS materialized
|
|
1948
|
+
JOIN oh_authority_operations AS operation
|
|
1949
|
+
ON operation.operation_sha256 = materialized.operation_sha256
|
|
1950
|
+
WHERE operation.space_id = ? AND operation.sequence <= ?
|
|
1951
|
+
AND (SELECT coalesce(sum(${OPERATION_RECORD_RESPONSE_BYTES}), 0)
|
|
1952
|
+
FROM oh_authority_operation_records AS materialized
|
|
1953
|
+
JOIN oh_authority_operations AS owner
|
|
1954
|
+
ON owner.operation_sha256 = materialized.operation_sha256
|
|
1955
|
+
WHERE owner.space_id = ? AND owner.sequence <= ?) <= ?
|
|
1956
|
+
ORDER BY operation.sequence, materialized.ordinal`, args: [
|
|
1957
|
+
this.binding.spaceId,
|
|
1958
|
+
snapshot.head.sequence,
|
|
1959
|
+
this.binding.spaceId,
|
|
1960
|
+
snapshot.head.sequence,
|
|
1961
|
+
OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes
|
|
1962
|
+
] },
|
|
1963
|
+
{ sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
1964
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }
|
|
1965
|
+
], "read");
|
|
1966
|
+
if (verificationResults.length !== 6) {
|
|
1967
|
+
throw new OhIntegrityError("The remote authority returned an incomplete verification batch.");
|
|
1968
|
+
}
|
|
1969
|
+
const [sizeResult, operationResult, recordResult, dependencyResult, operationRecordResult, headResult] = verificationResults;
|
|
1970
|
+
const sizeRow = sizeResult.rows[0];
|
|
1971
|
+
const operationCount = sizeRow === undefined ? null : integer(rowValue(sizeRow, "operation_count", 0));
|
|
1972
|
+
const minimumValue = sizeRow === undefined ? undefined : rowValue(sizeRow, "minimum", 1);
|
|
1973
|
+
const maximumValue = sizeRow === undefined ? undefined : rowValue(sizeRow, "maximum", 2);
|
|
1974
|
+
const minimumSequence = sizeRow === undefined ? null : integer(rowValue(sizeRow, "minimum", 1));
|
|
1975
|
+
const maximumSequence = sizeRow === undefined ? null : integer(rowValue(sizeRow, "maximum", 2));
|
|
1976
|
+
const historyBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "canonical_bytes", 3));
|
|
1977
|
+
const operationResponseBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "operation_response_bytes", 4));
|
|
1978
|
+
const recordResponseBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "record_response_bytes", 5));
|
|
1979
|
+
const dependencyResponseBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "dependency_response_bytes", 6));
|
|
1980
|
+
const operationRecordResponseBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "operation_record_response_bytes", 7));
|
|
1981
|
+
if (historyBytes === null || historyBytes > OH_LIBSQL_STORE_LIMITS_V1.historyBytes || operationResponseBytes === null || operationResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes || recordResponseBytes === null || recordResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes || dependencyResponseBytes === null || dependencyResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes || operationRecordResponseBytes === null || operationRecordResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes) {
|
|
1982
|
+
throw new RangeError("The libSQL authority exceeds its provider-safe verification bounds.");
|
|
1983
|
+
}
|
|
1984
|
+
if (operationCount !== snapshot.head.sequence || snapshot.head.sequence === 0 && (minimumValue !== null || maximumValue !== null) || snapshot.head.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== snapshot.head.sequence) || operationResult.rows.length !== operationCount) {
|
|
1985
|
+
throw new OhIntegrityError("The remote operation history does not exactly cover its verified head.");
|
|
1986
|
+
}
|
|
1987
|
+
const headRow = headResult.rows[0];
|
|
1988
|
+
if (headRow === undefined)
|
|
1989
|
+
throw new OhIntegrityError("The remote authority lost its head during verification.");
|
|
1990
|
+
if (canonicalJson(parseHeadRow(headRow)) !== canonicalJson(snapshot.head)) {
|
|
1991
|
+
throw new OhConflictError("The remote authority head changed during verification.");
|
|
1992
|
+
}
|
|
1993
|
+
const operations = operationResult.rows.map((row) => {
|
|
1994
|
+
return parseOperationRow(row, { spaceId: this.binding.spaceId });
|
|
1995
|
+
});
|
|
1996
|
+
const replayed = replayOhOperationsV1(this.binding.spaceId, operations);
|
|
1997
|
+
if (canonicalJson(replayed) !== canonicalJson(snapshot)) {
|
|
1998
|
+
throw new OhIntegrityError("Remote operation replay changed during materialization verification.");
|
|
1999
|
+
}
|
|
2000
|
+
const materializedBy = new Map;
|
|
2001
|
+
for (const operation of operations) {
|
|
2002
|
+
for (const change of operation.changes) {
|
|
2003
|
+
const key = change.kind === "put" ? change.record.key : change.key;
|
|
2004
|
+
if (change.kind === "put")
|
|
2005
|
+
materializedBy.set(key, { operationSha256: operation.operationSha256, sequence: operation.sequence });
|
|
2006
|
+
else
|
|
2007
|
+
materializedBy.delete(key);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
const records = recordResult.rows.map((row) => {
|
|
2011
|
+
const json = rowValue(row, "record_json", 3);
|
|
2012
|
+
if (typeof json !== "string")
|
|
2013
|
+
throw new OhIntegrityError("A materialized remote record is not JSON text.");
|
|
2014
|
+
let value;
|
|
2015
|
+
try {
|
|
2016
|
+
value = JSON.parse(json);
|
|
2017
|
+
} catch {
|
|
2018
|
+
throw new OhIntegrityError("A materialized remote record is invalid.");
|
|
2019
|
+
}
|
|
2020
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
2021
|
+
const provenance = record === null ? undefined : materializedBy.get(record.key);
|
|
2022
|
+
if (record === null || canonicalJson(record) !== json || rowValue(row, "record_key", 0) !== record.key || rowValue(row, "kind", 1) !== record.kind || rowValue(row, "record_sha256", 2) !== record.recordSha256 || provenance === undefined || rowValue(row, "operation_sha256", 4) !== provenance.operationSha256 || integer(rowValue(row, "sequence", 5)) !== provenance.sequence) {
|
|
2023
|
+
throw new OhIntegrityError("A materialized remote record differs from operation replay.");
|
|
2024
|
+
}
|
|
2025
|
+
return record;
|
|
2026
|
+
});
|
|
2027
|
+
if (canonicalJson(records) !== canonicalJson(snapshot.records)) {
|
|
2028
|
+
throw new OhIntegrityError("Remote materialized records do not match operation replay.");
|
|
2029
|
+
}
|
|
2030
|
+
const dependencyRows = dependencyResult.rows.map((row) => ({
|
|
2031
|
+
dependency_key: rowValue(row, "dependency_key", 1),
|
|
2032
|
+
record_key: rowValue(row, "record_key", 0)
|
|
2033
|
+
}));
|
|
2034
|
+
const expectedDependencies = snapshot.records.flatMap((record) => record.dependencies.map((dependency) => ({ dependency_key: dependency, record_key: record.key })));
|
|
2035
|
+
if (canonicalJson(dependencyRows) !== canonicalJson(expectedDependencies)) {
|
|
2036
|
+
throw new OhIntegrityError("Remote materialized dependencies do not match operation replay.");
|
|
2037
|
+
}
|
|
2038
|
+
const operationRecordRows = operationRecordResult.rows.map((row) => ({
|
|
2039
|
+
change_kind: rowValue(row, "change_kind", 4),
|
|
2040
|
+
operation_sha256: rowValue(row, "operation_sha256", 1),
|
|
2041
|
+
ordinal: integer(rowValue(row, "ordinal", 2)),
|
|
2042
|
+
record_key: rowValue(row, "record_key", 3),
|
|
2043
|
+
record_sha256: rowValue(row, "record_sha256", 5),
|
|
2044
|
+
space_id: rowValue(row, "space_id", 0)
|
|
2045
|
+
}));
|
|
2046
|
+
const expectedOperationRecords = operations.flatMap((operation) => operation.changes.map((change, ordinal) => ({
|
|
2047
|
+
change_kind: change.kind,
|
|
2048
|
+
operation_sha256: operation.operationSha256,
|
|
2049
|
+
ordinal,
|
|
2050
|
+
record_key: change.kind === "put" ? change.record.key : change.key,
|
|
2051
|
+
record_sha256: change.kind === "put" ? change.record.recordSha256 : change.priorSha256,
|
|
2052
|
+
space_id: this.binding.spaceId
|
|
2053
|
+
})));
|
|
2054
|
+
if (canonicalJson(operationRecordRows) !== canonicalJson(expectedOperationRecords)) {
|
|
2055
|
+
throw new OhIntegrityError("Remote operation-record rows do not match operation replay.");
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
async#operationById(operationId) {
|
|
2059
|
+
const row = await queryOne(this.#client, { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
2060
|
+
WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId] });
|
|
2061
|
+
if (row === null)
|
|
2062
|
+
return null;
|
|
2063
|
+
return parseOperationRow(row, { operationId, spaceId: this.binding.spaceId });
|
|
2064
|
+
}
|
|
2065
|
+
async#commitPreflight(operationId) {
|
|
2066
|
+
const results = await this.#client.batch([
|
|
2067
|
+
{ sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
2068
|
+
WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId] },
|
|
2069
|
+
{ sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
2070
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] },
|
|
2071
|
+
{ sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] }
|
|
2072
|
+
], "read");
|
|
2073
|
+
if (results.length !== 3)
|
|
2074
|
+
throw new OhIntegrityError("The remote authority returned an incomplete commit preflight.");
|
|
2075
|
+
const [duplicateResult, headResult, purgeResult] = results;
|
|
2076
|
+
const headRow = headResult.rows[0];
|
|
2077
|
+
if (headRow === undefined) {
|
|
2078
|
+
const purgeRow = purgeResult.rows[0];
|
|
2079
|
+
if (purgeRow !== undefined) {
|
|
2080
|
+
const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256);
|
|
2081
|
+
this.#purged = purge;
|
|
2082
|
+
throw new OhPurgedSpaceError(purge);
|
|
2083
|
+
}
|
|
2084
|
+
throw new OhIntegrityError("The remote space disappeared during commit preflight.");
|
|
2085
|
+
}
|
|
2086
|
+
const duplicateRow = duplicateResult.rows[0];
|
|
2087
|
+
return { current: parseHeadRow(headRow), duplicate: duplicateRow === undefined ? null : parseOperationRow(duplicateRow, { operationId, spaceId: this.binding.spaceId }) };
|
|
2088
|
+
}
|
|
2089
|
+
async#assertOperationReachable(operation, expectedHead) {
|
|
2090
|
+
if (operation.sequence < 1 || operation.sequence > expectedHead.sequence) {
|
|
2091
|
+
throw new OhIntegrityError("A remote idempotent operation is not reachable from the current head.");
|
|
2092
|
+
}
|
|
2093
|
+
const results = await this.#client.batch([
|
|
2094
|
+
{ sql: `SELECT * FROM (WITH RECURSIVE authority_chain(sequence, operation_sha256) AS (
|
|
2095
|
+
SELECT sequence, operation_sha256 FROM oh_authority_operations
|
|
2096
|
+
WHERE space_id = ? AND sequence = ? AND operation_sha256 = ?
|
|
2097
|
+
UNION ALL
|
|
2098
|
+
SELECT candidate.sequence, candidate.operation_sha256
|
|
2099
|
+
FROM oh_authority_operations AS candidate
|
|
2100
|
+
JOIN authority_chain AS prior
|
|
2101
|
+
ON candidate.space_id = ? AND candidate.sequence = prior.sequence + 1
|
|
2102
|
+
AND candidate.parent_operation_sha256 = prior.operation_sha256
|
|
2103
|
+
WHERE candidate.sequence <= ?
|
|
2104
|
+
) SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum,
|
|
2105
|
+
(SELECT operation_sha256 FROM authority_chain ORDER BY sequence DESC LIMIT 1) AS terminal_sha256
|
|
2106
|
+
FROM authority_chain)`, args: [
|
|
2107
|
+
this.binding.spaceId,
|
|
2108
|
+
operation.sequence,
|
|
2109
|
+
operation.operationSha256,
|
|
2110
|
+
this.binding.spaceId,
|
|
2111
|
+
expectedHead.sequence
|
|
2112
|
+
] },
|
|
2113
|
+
{ sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
2114
|
+
WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, expectedHead.sequence] },
|
|
2115
|
+
{ sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
2116
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] },
|
|
2117
|
+
{ sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] }
|
|
2118
|
+
], "read");
|
|
2119
|
+
if (results.length !== 4)
|
|
2120
|
+
throw new OhIntegrityError("The remote authority returned an incomplete reachability proof.");
|
|
2121
|
+
const [chainResult, terminalResult, headResult, purgeResult] = results;
|
|
2122
|
+
const headRow = headResult.rows[0];
|
|
2123
|
+
if (headRow === undefined) {
|
|
2124
|
+
const purgeRow = purgeResult.rows[0];
|
|
2125
|
+
if (purgeRow !== undefined) {
|
|
2126
|
+
const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256);
|
|
2127
|
+
this.#purged = purge;
|
|
2128
|
+
throw new OhPurgedSpaceError(purge);
|
|
2129
|
+
}
|
|
2130
|
+
throw new OhIntegrityError("The remote space disappeared during an idempotency proof.");
|
|
2131
|
+
}
|
|
2132
|
+
const current = parseHeadRow(headRow);
|
|
2133
|
+
if (canonicalJson(current) !== canonicalJson(expectedHead)) {
|
|
2134
|
+
throw new OhConflictError("The remote space head changed during an idempotency proof.");
|
|
2135
|
+
}
|
|
2136
|
+
const chain = chainResult.rows[0];
|
|
2137
|
+
const count = chain === undefined ? null : integer(rowValue(chain, "count", 0));
|
|
2138
|
+
const minimum = chain === undefined ? null : integer(rowValue(chain, "minimum", 1));
|
|
2139
|
+
const maximum = chain === undefined ? null : integer(rowValue(chain, "maximum", 2));
|
|
2140
|
+
const terminalSha256 = chain === undefined ? null : rowValue(chain, "terminal_sha256", 3);
|
|
2141
|
+
if (count !== expectedHead.sequence - operation.sequence + 1 || minimum !== operation.sequence || maximum !== expectedHead.sequence || terminalSha256 !== expectedHead.operationSha256) {
|
|
2142
|
+
throw new OhIntegrityError("A remote idempotent operation has no exact path to the current head.");
|
|
2143
|
+
}
|
|
2144
|
+
const terminalRow = terminalResult.rows[0];
|
|
2145
|
+
if (terminalRow === undefined || expectedHead.operationSha256 === null) {
|
|
2146
|
+
throw new OhIntegrityError("The remote current head operation is missing.");
|
|
2147
|
+
}
|
|
2148
|
+
const terminal = parseOperationRow(terminalRow, {
|
|
2149
|
+
operationSha256: expectedHead.operationSha256,
|
|
2150
|
+
spaceId: this.binding.spaceId
|
|
2151
|
+
});
|
|
2152
|
+
if (terminal.sequence !== expectedHead.sequence || terminal.graphRevisionSha256 !== expectedHead.graphRevisionSha256 || terminal.recordsSha256 !== expectedHead.recordsSha256) {
|
|
2153
|
+
throw new OhIntegrityError("The remote space head differs from its terminal canonical operation.");
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
async commit(input) {
|
|
2157
|
+
this.#assertOpen();
|
|
2158
|
+
const actorId = safeCode(input.actorId);
|
|
2159
|
+
const operationId = safeCode(input.operationId);
|
|
2160
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
2161
|
+
if (actorId === null || operationId === null || changes.length === 0)
|
|
2162
|
+
throw new TypeError("Invalid Oh commit input.");
|
|
2163
|
+
if (changes.length > OH_LIBSQL_STORE_LIMITS_V1.changesPerCommit) {
|
|
2164
|
+
throw new RangeError("A direct libSQL commit exceeds its change-count bound.");
|
|
2165
|
+
}
|
|
2166
|
+
const dependencies = changes.reduce((count, change) => count + (change.kind === "put" ? change.record.dependencies.length : 0), 0);
|
|
2167
|
+
if (dependencies > OH_LIBSQL_STORE_LIMITS_V1.dependenciesPerCommit) {
|
|
2168
|
+
throw new RangeError("A direct libSQL commit exceeds its dependency-count bound.");
|
|
2169
|
+
}
|
|
2170
|
+
const { current, duplicate } = await this.#commitPreflight(operationId);
|
|
2171
|
+
if (duplicate !== null) {
|
|
2172
|
+
await this.#assertOperationReachable(duplicate, current);
|
|
2173
|
+
if (duplicate.actorId !== actorId || canonicalJson(duplicate.changes) !== canonicalJson(changes)) {
|
|
2174
|
+
throw new OhConflictError("The operation ID is already bound to different content.");
|
|
2175
|
+
}
|
|
2176
|
+
return duplicate;
|
|
2177
|
+
}
|
|
2178
|
+
if (!Number.isSafeInteger(input.expectedHead.generation) || input.expectedHead.generation < 0 || current.generation !== input.expectedHead.generation || current.operationSha256 !== input.expectedHead.operationSha256) {
|
|
2179
|
+
throw new OhConflictError("The expected head does not match the current remote space head.");
|
|
2180
|
+
}
|
|
2181
|
+
const snapshot = await this.#currentMaterializedSnapshot(current, OH_GRAPH_LIMITS_V1.recordsPerSnapshot);
|
|
2182
|
+
const transition = transitionOhSnapshotV1({
|
|
2183
|
+
actorId,
|
|
2184
|
+
changes,
|
|
2185
|
+
instant: input.instant ?? canonicalNow(),
|
|
2186
|
+
operationId,
|
|
2187
|
+
snapshot,
|
|
2188
|
+
spaceId: this.binding.spaceId
|
|
2189
|
+
});
|
|
2190
|
+
const operation = transition.operation;
|
|
2191
|
+
if (utf8ByteLength(canonicalJson(operation)) > OH_LIBSQL_STORE_LIMITS_V1.operationBytes) {
|
|
2192
|
+
throw new RangeError("A direct libSQL operation exceeds its canonical byte bound.");
|
|
2193
|
+
}
|
|
2194
|
+
const existsOperation = "EXISTS (SELECT 1 FROM oh_authority_operations WHERE operation_sha256 = ?)";
|
|
2195
|
+
const statements = [{
|
|
2196
|
+
sql: `INSERT INTO oh_authority_operations(operation_sha256, space_id, sequence,
|
|
2197
|
+
operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256,
|
|
2198
|
+
operation_json, instant)
|
|
2199
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
2200
|
+
WHERE EXISTS (SELECT 1 FROM oh_authority_spaces
|
|
2201
|
+
WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?)`,
|
|
2202
|
+
args: [
|
|
2203
|
+
operation.operationSha256,
|
|
2204
|
+
this.binding.spaceId,
|
|
2205
|
+
operation.sequence,
|
|
2206
|
+
operation.operationId,
|
|
2207
|
+
operation.parentOperationSha256,
|
|
2208
|
+
operation.graphRevisionSha256,
|
|
2209
|
+
operation.recordsSha256,
|
|
2210
|
+
canonicalJson(operation),
|
|
2211
|
+
operation.instant,
|
|
2212
|
+
this.binding.spaceId,
|
|
2213
|
+
current.generation,
|
|
2214
|
+
current.operationSha256
|
|
2215
|
+
]
|
|
2216
|
+
}];
|
|
2217
|
+
for (const [ordinal, change] of operation.changes.entries()) {
|
|
2218
|
+
const key = change.kind === "put" ? change.record.key : change.key;
|
|
2219
|
+
const digest = change.kind === "put" ? change.record.recordSha256 : change.priorSha256;
|
|
2220
|
+
statements.push({
|
|
2221
|
+
sql: `INSERT INTO oh_authority_operation_records(space_id, operation_sha256,
|
|
2222
|
+
ordinal, record_key, change_kind, record_sha256)
|
|
2223
|
+
SELECT ?, ?, ?, ?, ?, ? WHERE ${existsOperation}`,
|
|
2224
|
+
args: [
|
|
2225
|
+
this.binding.spaceId,
|
|
2226
|
+
operation.operationSha256,
|
|
2227
|
+
ordinal,
|
|
2228
|
+
key,
|
|
2229
|
+
change.kind,
|
|
2230
|
+
digest,
|
|
2231
|
+
operation.operationSha256
|
|
2232
|
+
]
|
|
2233
|
+
});
|
|
2234
|
+
statements.push({ sql: `DELETE FROM oh_authority_dependencies WHERE space_id = ? AND record_key = ?
|
|
2235
|
+
AND ${existsOperation}`, args: [this.binding.spaceId, key, operation.operationSha256] });
|
|
2236
|
+
if (change.kind === "put") {
|
|
2237
|
+
statements.push({
|
|
2238
|
+
sql: `INSERT INTO oh_authority_records(space_id, record_key, kind,
|
|
2239
|
+
record_sha256, record_json, operation_sha256, sequence)
|
|
2240
|
+
SELECT ?, ?, ?, ?, ?, ?, ? WHERE ${existsOperation}
|
|
2241
|
+
ON CONFLICT(space_id, record_key) DO UPDATE SET kind = excluded.kind,
|
|
2242
|
+
record_sha256 = excluded.record_sha256, record_json = excluded.record_json,
|
|
2243
|
+
operation_sha256 = excluded.operation_sha256, sequence = excluded.sequence`,
|
|
2244
|
+
args: [
|
|
2245
|
+
this.binding.spaceId,
|
|
2246
|
+
key,
|
|
2247
|
+
change.record.kind,
|
|
2248
|
+
change.record.recordSha256,
|
|
2249
|
+
canonicalJson(change.record),
|
|
2250
|
+
operation.operationSha256,
|
|
2251
|
+
operation.sequence,
|
|
2252
|
+
operation.operationSha256
|
|
2253
|
+
]
|
|
2254
|
+
});
|
|
2255
|
+
} else {
|
|
2256
|
+
statements.push({
|
|
2257
|
+
sql: `DELETE FROM oh_authority_records WHERE space_id = ? AND record_key = ?
|
|
2258
|
+
AND record_sha256 = ? AND ${existsOperation}`,
|
|
2259
|
+
args: [this.binding.spaceId, key, change.priorSha256, operation.operationSha256]
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
for (const change of operation.changes) {
|
|
2264
|
+
if (change.kind !== "put")
|
|
2265
|
+
continue;
|
|
2266
|
+
for (const dependency of change.record.dependencies) {
|
|
2267
|
+
statements.push({
|
|
2268
|
+
sql: `INSERT INTO oh_authority_dependencies(space_id, record_key, dependency_key)
|
|
2269
|
+
SELECT ?, ?, ? WHERE ${existsOperation}`,
|
|
2270
|
+
args: [this.binding.spaceId, change.record.key, dependency, operation.operationSha256]
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
statements.push({
|
|
2275
|
+
sql: `UPDATE oh_authority_spaces SET generation = ?, head_operation_sha256 = ?,
|
|
2276
|
+
graph_revision_sha256 = ?, records_sha256 = ?, sequence = ?, updated_at = ?
|
|
2277
|
+
WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ? AND ${existsOperation}`,
|
|
2278
|
+
args: [
|
|
2279
|
+
operation.sequence,
|
|
2280
|
+
operation.operationSha256,
|
|
2281
|
+
operation.graphRevisionSha256,
|
|
2282
|
+
operation.recordsSha256,
|
|
2283
|
+
operation.sequence,
|
|
2284
|
+
operation.instant,
|
|
2285
|
+
this.binding.spaceId,
|
|
2286
|
+
current.generation,
|
|
2287
|
+
current.operationSha256,
|
|
2288
|
+
operation.operationSha256
|
|
2289
|
+
]
|
|
2290
|
+
});
|
|
2291
|
+
statements.push({
|
|
2292
|
+
sql: `INSERT INTO oh_authority_commit_guards(value)
|
|
2293
|
+
SELECT 'invalid' WHERE NOT EXISTS (SELECT 1 FROM oh_authority_spaces
|
|
2294
|
+
WHERE space_id = ? AND generation = ? AND head_operation_sha256 = ?)`,
|
|
2295
|
+
args: [this.binding.spaceId, operation.sequence, operation.operationSha256]
|
|
2296
|
+
});
|
|
2297
|
+
statements.push({ sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations
|
|
2298
|
+
WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId] });
|
|
2299
|
+
statements.push({ sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence
|
|
2300
|
+
FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] });
|
|
2301
|
+
let writeResults;
|
|
2302
|
+
try {
|
|
2303
|
+
writeResults = await this.#client.batch(statements, "write");
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
const raced = await this.#operationById(operationId);
|
|
2306
|
+
const head = await this.head();
|
|
2307
|
+
if (raced !== null && raced.actorId === actorId && canonicalJson(raced.changes) === canonicalJson(changes)) {
|
|
2308
|
+
await this.#assertOperationReachable(raced, head);
|
|
2309
|
+
return raced;
|
|
2310
|
+
}
|
|
2311
|
+
if (head.operationSha256 !== current.operationSha256) {
|
|
2312
|
+
throw new OhConflictError("The remote space head changed while committing.");
|
|
2313
|
+
}
|
|
2314
|
+
throw error;
|
|
2315
|
+
}
|
|
2316
|
+
if (writeResults.length !== statements.length) {
|
|
2317
|
+
throw new OhIntegrityError("The remote authority returned an incomplete commit result batch.");
|
|
2318
|
+
}
|
|
2319
|
+
const persistedRow = writeResults.at(-2)?.rows[0];
|
|
2320
|
+
const persistedHeadRow = writeResults.at(-1)?.rows[0];
|
|
2321
|
+
if (persistedRow === undefined || persistedHeadRow === undefined) {
|
|
2322
|
+
throw new OhIntegrityError("The remote authority omitted its persisted commit result.");
|
|
2323
|
+
}
|
|
2324
|
+
const persisted = parseOperationRow(persistedRow, { operationId, spaceId: this.binding.spaceId });
|
|
2325
|
+
const persistedHead = parseHeadRow(persistedHeadRow);
|
|
2326
|
+
if (canonicalJson(persisted) !== canonicalJson(operation) || persistedHead.operationSha256 !== operation.operationSha256 || persistedHead.sequence !== operation.sequence || persistedHead.graphRevisionSha256 !== operation.graphRevisionSha256 || persistedHead.recordsSha256 !== operation.recordsSha256) {
|
|
2327
|
+
throw new OhIntegrityError("The remote authority did not persist the committed operation exactly.");
|
|
2328
|
+
}
|
|
2329
|
+
return persisted;
|
|
2330
|
+
}
|
|
2331
|
+
async exportDependencyClosure(input) {
|
|
2332
|
+
if (!this.binding.profile.capabilities.dependencyClosureExport) {
|
|
2333
|
+
throw new OhProfileError("This remote profile does not permit dependency-closure export.");
|
|
2334
|
+
}
|
|
2335
|
+
const snapshot = await this.snapshot({
|
|
2336
|
+
...input.head === undefined ? {} : { head: input.head },
|
|
2337
|
+
...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }
|
|
2338
|
+
});
|
|
2339
|
+
return createOhDependencyClosureV1({
|
|
2340
|
+
binding: this.binding,
|
|
2341
|
+
...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords },
|
|
2342
|
+
roots: input.roots,
|
|
2343
|
+
snapshot
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
async verify() {
|
|
2347
|
+
this.#assertOpen();
|
|
2348
|
+
const snapshot = await this.snapshot();
|
|
2349
|
+
await this.#assertMaterializedSnapshot(snapshot);
|
|
2350
|
+
return {
|
|
2351
|
+
head: snapshot.head,
|
|
2352
|
+
integrity: "verified",
|
|
2353
|
+
operations: snapshot.head.sequence,
|
|
2354
|
+
records: snapshot.records.length,
|
|
2355
|
+
v: 1
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
async#assertPurgeComplete(expected) {
|
|
2359
|
+
await assertRemotePurgeComplete(this.#client, this.binding, expected);
|
|
2360
|
+
}
|
|
2361
|
+
async purgeWorkingSpace(purgedAt) {
|
|
2362
|
+
this.#assertOpen();
|
|
2363
|
+
if (this.binding.profile.profileKind !== "working" || !this.binding.profile.capabilities.wholeSpacePurge) {
|
|
2364
|
+
throw new OhProfileError("Whole-space purge requires a bound working profile.");
|
|
2365
|
+
}
|
|
2366
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
2367
|
+
const existing = await this.#readPurge();
|
|
2368
|
+
if (existing !== null) {
|
|
2369
|
+
await this.#assertPurgeComplete(existing);
|
|
2370
|
+
this.#purged = existing;
|
|
2371
|
+
return existing;
|
|
2372
|
+
}
|
|
2373
|
+
const head = await this.head();
|
|
2374
|
+
const receipt = createOhSpacePurgeReceiptV1({ binding: this.binding, priorHead: head, purgedAt });
|
|
2375
|
+
const receiptExists = "EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ? AND receipt_sha256 = ?)";
|
|
2376
|
+
const statements = [{
|
|
2377
|
+
sql: `INSERT INTO oh_authority_purges(space_id,
|
|
2378
|
+
binding_sha256, prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json)
|
|
2379
|
+
SELECT ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (SELECT 1 FROM oh_authority_spaces
|
|
2380
|
+
WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?)
|
|
2381
|
+
AND EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ? AND binding_sha256 = ?)`,
|
|
2382
|
+
args: [
|
|
2383
|
+
this.binding.spaceId,
|
|
2384
|
+
this.binding.bindingSha256,
|
|
2385
|
+
head.operationSha256,
|
|
2386
|
+
head.sequence,
|
|
2387
|
+
receipt.purgedAt,
|
|
2388
|
+
receipt.receiptSha256,
|
|
2389
|
+
canonicalJson(receipt),
|
|
2390
|
+
this.binding.spaceId,
|
|
2391
|
+
head.generation,
|
|
2392
|
+
head.operationSha256,
|
|
2393
|
+
this.binding.spaceId,
|
|
2394
|
+
this.binding.bindingSha256
|
|
2395
|
+
]
|
|
2396
|
+
}];
|
|
2397
|
+
const guardedDelete = (table) => ({
|
|
2398
|
+
sql: `DELETE FROM ${table} WHERE space_id = ? AND ${receiptExists}`,
|
|
2399
|
+
args: [this.binding.spaceId, this.binding.spaceId, receipt.receiptSha256]
|
|
2400
|
+
});
|
|
2401
|
+
statements.push({
|
|
2402
|
+
sql: `DELETE FROM oh_authority_operation_records
|
|
2403
|
+
WHERE operation_sha256 IN (SELECT operation_sha256 FROM oh_authority_operations WHERE space_id = ?)
|
|
2404
|
+
AND ${receiptExists}`,
|
|
2405
|
+
args: [this.binding.spaceId, this.binding.spaceId, receipt.receiptSha256]
|
|
2406
|
+
});
|
|
2407
|
+
statements.push(guardedDelete("oh_authority_dependencies"));
|
|
2408
|
+
statements.push(guardedDelete("oh_authority_records"));
|
|
2409
|
+
statements.push(guardedDelete("oh_authority_operations"));
|
|
2410
|
+
statements.push(guardedDelete("oh_authority_bindings"));
|
|
2411
|
+
statements.push(guardedDelete("oh_authority_spaces"));
|
|
2412
|
+
statements.push({
|
|
2413
|
+
sql: `INSERT INTO oh_authority_commit_guards(value)
|
|
2414
|
+
SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
2415
|
+
OR EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?)
|
|
2416
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operations WHERE space_id = ?)
|
|
2417
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operation_records WHERE space_id = ?)
|
|
2418
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operation_records AS materialized
|
|
2419
|
+
LEFT JOIN oh_authority_operations AS operation
|
|
2420
|
+
ON operation.operation_sha256 = materialized.operation_sha256
|
|
2421
|
+
WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id)
|
|
2422
|
+
OR EXISTS (SELECT 1 FROM oh_authority_records WHERE space_id = ?)
|
|
2423
|
+
OR EXISTS (SELECT 1 FROM oh_authority_dependencies WHERE space_id = ?)
|
|
2424
|
+
OR NOT ${receiptExists}`,
|
|
2425
|
+
args: [
|
|
2426
|
+
this.binding.spaceId,
|
|
2427
|
+
this.binding.spaceId,
|
|
2428
|
+
this.binding.spaceId,
|
|
2429
|
+
this.binding.spaceId,
|
|
2430
|
+
this.binding.spaceId,
|
|
2431
|
+
this.binding.spaceId,
|
|
2432
|
+
this.binding.spaceId,
|
|
2433
|
+
receipt.receiptSha256
|
|
2434
|
+
]
|
|
2435
|
+
});
|
|
2436
|
+
try {
|
|
2437
|
+
await this.#client.batch(statements, "write");
|
|
2438
|
+
} catch {
|
|
2439
|
+
const raced = await this.#readPurge();
|
|
2440
|
+
if (raced !== null) {
|
|
2441
|
+
await this.#assertPurgeComplete(raced);
|
|
2442
|
+
this.#purged = raced;
|
|
2443
|
+
return raced;
|
|
2444
|
+
}
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
const persisted = await this.#readPurge();
|
|
2448
|
+
if (persisted !== null) {
|
|
2449
|
+
await this.#assertPurgeComplete(persisted);
|
|
2450
|
+
this.#purged = persisted;
|
|
2451
|
+
return persisted;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
throw new OhConflictError("The remote working space changed repeatedly while purging.");
|
|
2455
|
+
}
|
|
2456
|
+
async close() {
|
|
2457
|
+
if (this.#closed)
|
|
2458
|
+
return;
|
|
2459
|
+
this.#closed = true;
|
|
2460
|
+
if (this.#closeClient)
|
|
2461
|
+
this.#client.close?.();
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
function bindOhLibSqlStoreAuthorityV1(client, binding, profile, closeClient) {
|
|
2465
|
+
const authority = new OhLibSqlStoreV1(client, binding, closeClient);
|
|
2466
|
+
const store = Object.freeze({
|
|
2467
|
+
binding,
|
|
2468
|
+
changesSince: (from, changeOptions) => authority.changesSince(from, changeOptions),
|
|
2469
|
+
close: () => authority.close(),
|
|
2470
|
+
commit: (input) => authority.commit(input),
|
|
2471
|
+
exportDependencyClosure: (input) => authority.exportDependencyClosure(input),
|
|
2472
|
+
head: () => authority.head(),
|
|
2473
|
+
snapshot: (snapshotOptions) => authority.snapshot(snapshotOptions),
|
|
2474
|
+
verify: () => authority.verify()
|
|
2475
|
+
});
|
|
2476
|
+
let purge = null;
|
|
2477
|
+
const host = Object.freeze({
|
|
2478
|
+
binding,
|
|
2479
|
+
purgeWorkingSpace: async (input) => {
|
|
2480
|
+
if (profile.profileKind !== "working" || !profile.capabilities.wholeSpacePurge) {
|
|
2481
|
+
throw new OhProfileError("This host handle is not bound to a purgeable working profile.");
|
|
2482
|
+
}
|
|
2483
|
+
if (purge !== null)
|
|
2484
|
+
return purge;
|
|
2485
|
+
purge = await authority.purgeWorkingSpace(input.purgedAt ?? canonicalNow());
|
|
2486
|
+
return purge;
|
|
2487
|
+
}
|
|
2488
|
+
});
|
|
2489
|
+
return Object.freeze({ host, store });
|
|
2490
|
+
}
|
|
2491
|
+
async function createOhLibSqlStoreAuthorityV1(client, options = {}) {
|
|
2492
|
+
const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1);
|
|
2493
|
+
if (profile === null)
|
|
2494
|
+
throw new TypeError("Invalid libSQL store profile.");
|
|
2495
|
+
const spaceId = options.spaceId ?? "default";
|
|
2496
|
+
const binding = createOhStoreBindingV1({
|
|
2497
|
+
profile,
|
|
2498
|
+
realmId: options.realmId ?? `realm:${spaceId}`,
|
|
2499
|
+
spaceId,
|
|
2500
|
+
v: 1
|
|
2501
|
+
});
|
|
2502
|
+
await verifyAuthoritySchema(client);
|
|
2503
|
+
await initializeSpace(client, binding);
|
|
2504
|
+
return bindOhLibSqlStoreAuthorityV1(client, binding, profile, options.closeClient ?? false);
|
|
2505
|
+
}
|
|
2506
|
+
async function openExistingOhLibSqlStoreAuthorityV1(client, options = {}) {
|
|
2507
|
+
const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1);
|
|
2508
|
+
if (profile === null)
|
|
2509
|
+
throw new TypeError("Invalid libSQL store profile.");
|
|
2510
|
+
const spaceId = options.spaceId ?? "default";
|
|
2511
|
+
const binding = createOhStoreBindingV1({
|
|
2512
|
+
profile,
|
|
2513
|
+
realmId: options.realmId ?? `realm:${spaceId}`,
|
|
2514
|
+
spaceId,
|
|
2515
|
+
v: 1
|
|
2516
|
+
});
|
|
2517
|
+
await verifyAuthoritySchema(client);
|
|
2518
|
+
await requireExistingSpace(client, binding);
|
|
2519
|
+
return bindOhLibSqlStoreAuthorityV1(client, binding, profile, options.closeClient ?? false);
|
|
2520
|
+
}
|
|
2521
|
+
async function purgeOhLibSqlWorkingSpaceV1(client, options = {}) {
|
|
2522
|
+
const closeClient = options.closeClient ?? false;
|
|
2523
|
+
try {
|
|
2524
|
+
const profile = parseOhStoreProfileV1(options.profile ?? OH_WORKING_STORE_PROFILE_V1);
|
|
2525
|
+
if (profile === null)
|
|
2526
|
+
throw new TypeError("Invalid libSQL store profile.");
|
|
2527
|
+
if (profile.profileKind !== "working" || !profile.capabilities.wholeSpacePurge) {
|
|
2528
|
+
throw new OhProfileError("Whole-space purge requires a bound working profile.");
|
|
2529
|
+
}
|
|
2530
|
+
const spaceId = options.spaceId ?? "default";
|
|
2531
|
+
const binding = createOhStoreBindingV1({
|
|
2532
|
+
profile,
|
|
2533
|
+
realmId: options.realmId ?? `realm:${spaceId}`,
|
|
2534
|
+
spaceId,
|
|
2535
|
+
v: 1
|
|
2536
|
+
});
|
|
2537
|
+
const purgedAt = options.purgedAt ?? canonicalNow();
|
|
2538
|
+
await verifyAuthoritySchema(client);
|
|
2539
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
2540
|
+
const proof = await client.batch([
|
|
2541
|
+
{ sql: BINDING_ROW_SELECT, args: [binding.spaceId] },
|
|
2542
|
+
{ sql: SPACE_PURGE_PROOF_SELECT, args: [binding.spaceId] },
|
|
2543
|
+
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] }
|
|
2544
|
+
], "read");
|
|
2545
|
+
if (proof.length !== 3) {
|
|
2546
|
+
throw new OhIntegrityError("The remote authority returned an incomplete purge proof.");
|
|
2547
|
+
}
|
|
2548
|
+
const [bindingResult, spaceResult, purgeResult] = proof;
|
|
2549
|
+
const existingPurge = purgeResult.rows[0];
|
|
2550
|
+
if (existingPurge !== undefined) {
|
|
2551
|
+
const receipt2 = parsePurgeReceiptRow(existingPurge, binding.spaceId, binding.bindingSha256);
|
|
2552
|
+
await assertRemotePurgeComplete(client, binding, receipt2);
|
|
2553
|
+
return receipt2;
|
|
2554
|
+
}
|
|
2555
|
+
const bindingRow = bindingResult.rows[0];
|
|
2556
|
+
const spaceRow = spaceResult.rows[0];
|
|
2557
|
+
if (bindingRow === undefined !== (spaceRow === undefined)) {
|
|
2558
|
+
throw new OhIntegrityError("The remote authority has only half of its space binding.");
|
|
2559
|
+
}
|
|
2560
|
+
if (bindingRow !== undefined && spaceRow !== undefined) {
|
|
2561
|
+
const persisted2 = parseBindingRow(bindingRow, binding.spaceId);
|
|
2562
|
+
if (canonicalJson(persisted2) !== canonicalJson(binding)) {
|
|
2563
|
+
throw new OhProfileError("The remote space is bound to a different realm or profile.");
|
|
2564
|
+
}
|
|
2565
|
+
if (rowValue(spaceRow, "contract_id", 5) !== OH_CONTRACT_MANIFEST_V1.contractId) {
|
|
2566
|
+
throw new OhIntegrityError("The existing remote space uses a different Oh contract.");
|
|
2567
|
+
}
|
|
2568
|
+
parseHeadRow(spaceRow);
|
|
2569
|
+
const authority = new OhLibSqlStoreV1(client, binding, false);
|
|
2570
|
+
return await authority.purgeWorkingSpace(purgedAt);
|
|
2571
|
+
}
|
|
2572
|
+
const receipt = createOhSpacePurgeReceiptV1({
|
|
2573
|
+
binding,
|
|
2574
|
+
priorHead: emptyOhHeadV1(),
|
|
2575
|
+
purgedAt
|
|
2576
|
+
});
|
|
2577
|
+
const receiptJson = canonicalJson(receipt);
|
|
2578
|
+
try {
|
|
2579
|
+
await client.batch([
|
|
2580
|
+
{
|
|
2581
|
+
sql: `INSERT INTO oh_authority_purges(space_id, binding_sha256,
|
|
2582
|
+
prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json)
|
|
2583
|
+
SELECT ?, ?, NULL, 0, ?, ?, ?
|
|
2584
|
+
WHERE NOT EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
2585
|
+
AND NOT EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?)
|
|
2586
|
+
ON CONFLICT(space_id) DO NOTHING`,
|
|
2587
|
+
args: [
|
|
2588
|
+
binding.spaceId,
|
|
2589
|
+
binding.bindingSha256,
|
|
2590
|
+
receipt.purgedAt,
|
|
2591
|
+
receipt.receiptSha256,
|
|
2592
|
+
receiptJson,
|
|
2593
|
+
binding.spaceId,
|
|
2594
|
+
binding.spaceId
|
|
2595
|
+
]
|
|
2596
|
+
},
|
|
2597
|
+
{
|
|
2598
|
+
sql: `INSERT INTO oh_authority_commit_guards(value)
|
|
2599
|
+
SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
|
|
2600
|
+
OR EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?)
|
|
2601
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operations WHERE space_id = ?)
|
|
2602
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operation_records WHERE space_id = ?)
|
|
2603
|
+
OR EXISTS (SELECT 1 FROM oh_authority_records WHERE space_id = ?)
|
|
2604
|
+
OR EXISTS (SELECT 1 FROM oh_authority_dependencies WHERE space_id = ?)
|
|
2605
|
+
OR EXISTS (SELECT 1 FROM oh_authority_operation_records AS materialized
|
|
2606
|
+
LEFT JOIN oh_authority_operations AS operation
|
|
2607
|
+
ON operation.operation_sha256 = materialized.operation_sha256
|
|
2608
|
+
WHERE operation.operation_sha256 IS NULL
|
|
2609
|
+
OR operation.space_id <> materialized.space_id)
|
|
2610
|
+
OR NOT EXISTS (SELECT 1 FROM oh_authority_purges
|
|
2611
|
+
WHERE space_id = ? AND receipt_sha256 = ?)`,
|
|
2612
|
+
args: [
|
|
2613
|
+
binding.spaceId,
|
|
2614
|
+
binding.spaceId,
|
|
2615
|
+
binding.spaceId,
|
|
2616
|
+
binding.spaceId,
|
|
2617
|
+
binding.spaceId,
|
|
2618
|
+
binding.spaceId,
|
|
2619
|
+
binding.spaceId,
|
|
2620
|
+
receipt.receiptSha256
|
|
2621
|
+
]
|
|
2622
|
+
}
|
|
2623
|
+
], "write");
|
|
2624
|
+
} catch (error) {
|
|
2625
|
+
const recovery = await client.batch([
|
|
2626
|
+
{ sql: BINDING_ROW_SELECT, args: [binding.spaceId] },
|
|
2627
|
+
{ sql: SPACE_PURGE_PROOF_SELECT, args: [binding.spaceId] },
|
|
2628
|
+
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] }
|
|
2629
|
+
], "read");
|
|
2630
|
+
if (recovery.length !== 3) {
|
|
2631
|
+
throw new OhIntegrityError("The remote authority returned an incomplete purge recovery proof.");
|
|
2632
|
+
}
|
|
2633
|
+
const raced = recovery[2]?.rows[0];
|
|
2634
|
+
if (raced !== undefined) {
|
|
2635
|
+
const persisted2 = parsePurgeReceiptRow(raced, binding.spaceId, binding.bindingSha256);
|
|
2636
|
+
await assertRemotePurgeComplete(client, binding, persisted2);
|
|
2637
|
+
return persisted2;
|
|
2638
|
+
}
|
|
2639
|
+
if (recovery[0]?.rows[0] !== undefined || recovery[1]?.rows[0] !== undefined)
|
|
2640
|
+
continue;
|
|
2641
|
+
throw error;
|
|
2642
|
+
}
|
|
2643
|
+
const persisted = await queryOne(client, { sql: PURGE_ROW_SELECT, args: [binding.spaceId] });
|
|
2644
|
+
if (persisted === null)
|
|
2645
|
+
continue;
|
|
2646
|
+
const exact = parsePurgeReceiptRow(persisted, binding.spaceId, binding.bindingSha256);
|
|
2647
|
+
await assertRemotePurgeComplete(client, binding, exact);
|
|
2648
|
+
return exact;
|
|
2649
|
+
}
|
|
2650
|
+
throw new OhConflictError("The remote working space changed repeatedly while fencing purge.");
|
|
2651
|
+
} finally {
|
|
2652
|
+
if (closeClient)
|
|
2653
|
+
client.close?.();
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
export {
|
|
2657
|
+
purgeOhLibSqlWorkingSpaceV1,
|
|
2658
|
+
openExistingOhLibSqlStoreAuthorityV1,
|
|
2659
|
+
createOhLibSqlStoreAuthorityV1,
|
|
2660
|
+
bootstrapOhLibSqlAuthorityV1,
|
|
2661
|
+
OH_LIBSQL_STORE_LIMITS_V1
|
|
2662
|
+
};
|