@hraness/oh 0.3.2 → 0.4.0
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/README.md +164 -114
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +811 -97
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/graph.d.ts.map +1 -1
- package/dist/index.js +676 -61
- package/dist/libsql.d.ts.map +1 -1
- package/dist/libsql.js +162 -35
- package/dist/memory-page.js +2 -2
- package/dist/memory.d.ts +87 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +1106 -148
- package/dist/operation.d.ts +3 -1
- package/dist/operation.d.ts.map +1 -1
- package/dist/projection-public.js +2 -2
- package/dist/projection-suss.js +2 -2
- package/dist/sdk.js +780 -88
- package/dist/semantic-cloud.js +2 -2
- package/dist/semantic.js +2 -2
- package/dist/sqlite/index.js +1251 -306
- package/dist/sqlite/port.d.ts +31 -3
- package/dist/sqlite/port.d.ts.map +1 -1
- package/dist/sqlite/store.d.ts +18 -2
- package/dist/sqlite/store.d.ts.map +1 -1
- package/dist/store.d.ts +3 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +154 -32
- package/dist/sync.d.ts +7 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +668 -35
- package/package.json +5 -1
- package/skills/oh/SKILL.md +42 -16
- package/spec/README.md +2 -2
- package/spec/v1/memory.md +134 -16
- package/spec/v1/storage.md +8 -5
- package/spec/v1/store.md +20 -0
- package/spec/v1/sync.md +77 -8
- package/src/cli.test.ts +53 -1
- package/src/cli.ts +34 -8
- package/src/errors.test.ts +87 -0
- package/src/errors.ts +185 -0
- package/src/graph.ts +2 -2
- package/src/libsql.test.ts +36 -0
- package/src/libsql.ts +26 -5
- package/src/memory.test.ts +1488 -18
- package/src/memory.ts +1199 -122
- package/src/operation.ts +13 -3
- package/src/sqlite/port.test.ts +209 -0
- package/src/sqlite/port.ts +118 -4
- package/src/sqlite/store.test.ts +106 -1
- package/src/sqlite/store.ts +168 -30
- package/src/store.test.ts +12 -0
- package/src/store.ts +30 -20
- package/src/sync.test.ts +570 -2
- package/src/sync.ts +586 -36
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isOhConflictError,
|
|
5
|
+
isOhDependencyError,
|
|
6
|
+
isOhIntegrityError,
|
|
7
|
+
isOhOperationSizeError,
|
|
8
|
+
isOhProfileError,
|
|
9
|
+
OhConflictError,
|
|
10
|
+
OhDependencyError,
|
|
11
|
+
OhIntegrityError,
|
|
12
|
+
OhOperationSizeError,
|
|
13
|
+
OhProfileError,
|
|
14
|
+
} from "./errors";
|
|
15
|
+
|
|
16
|
+
const CORE_ERRORS = [
|
|
17
|
+
{
|
|
18
|
+
brand: "@hraness/oh/OhConflictError/v1",
|
|
19
|
+
ErrorClass: OhConflictError,
|
|
20
|
+
guard: isOhConflictError,
|
|
21
|
+
name: "OhConflictError",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
brand: "@hraness/oh/OhIntegrityError/v1",
|
|
25
|
+
ErrorClass: OhIntegrityError,
|
|
26
|
+
guard: isOhIntegrityError,
|
|
27
|
+
name: "OhIntegrityError",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
brand: "@hraness/oh/OhDependencyError/v1",
|
|
31
|
+
ErrorClass: OhDependencyError,
|
|
32
|
+
guard: isOhDependencyError,
|
|
33
|
+
name: "OhDependencyError",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
brand: "@hraness/oh/OhProfileError/v1",
|
|
37
|
+
ErrorClass: OhProfileError,
|
|
38
|
+
guard: isOhProfileError,
|
|
39
|
+
name: "OhProfileError",
|
|
40
|
+
},
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
describe("Oh public error identity", () => {
|
|
44
|
+
for (const { brand, ErrorClass, guard, name } of CORE_ERRORS) {
|
|
45
|
+
test(`brands ${name} without changing its public constructor`, () => {
|
|
46
|
+
const error = new ErrorClass("preserved message");
|
|
47
|
+
expect(Error.isError(error)).toBe(true);
|
|
48
|
+
expect(error).toBeInstanceOf(Error);
|
|
49
|
+
expect(error).toBeInstanceOf(ErrorClass);
|
|
50
|
+
expect(guard(error)).toBe(true);
|
|
51
|
+
expect(error).toMatchObject({ message: "preserved message", name });
|
|
52
|
+
expect(Object.keys(error)).toEqual(["name"]);
|
|
53
|
+
|
|
54
|
+
const copied = Object.create(Error.prototype) as Record<PropertyKey, unknown>;
|
|
55
|
+
Object.defineProperty(copied, Symbol.for(brand), {
|
|
56
|
+
configurable: false,
|
|
57
|
+
value: true,
|
|
58
|
+
writable: false,
|
|
59
|
+
});
|
|
60
|
+
expect(Error.isError(copied)).toBe(false);
|
|
61
|
+
expect(guard(copied)).toBe(false);
|
|
62
|
+
expect(copied instanceof ErrorClass).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test("preserves native subclass identity while exposing the branded base", () => {
|
|
67
|
+
class NarrowConflictError extends OhConflictError {}
|
|
68
|
+
|
|
69
|
+
const base = new OhConflictError("base");
|
|
70
|
+
const narrow = new NarrowConflictError("narrow");
|
|
71
|
+
expect(base).not.toBeInstanceOf(NarrowConflictError);
|
|
72
|
+
expect(narrow).toBeInstanceOf(NarrowConflictError);
|
|
73
|
+
expect(narrow).toBeInstanceOf(OhConflictError);
|
|
74
|
+
expect(isOhConflictError(narrow)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("does not make every operation-size error an instance of a subclass", () => {
|
|
78
|
+
class NarrowSizeError extends OhOperationSizeError {}
|
|
79
|
+
|
|
80
|
+
const base = new OhOperationSizeError(2, 1);
|
|
81
|
+
const narrow = new NarrowSizeError(2, 1);
|
|
82
|
+
expect(base).not.toBeInstanceOf(NarrowSizeError);
|
|
83
|
+
expect(narrow).toBeInstanceOf(NarrowSizeError);
|
|
84
|
+
expect(narrow).toBeInstanceOf(OhOperationSizeError);
|
|
85
|
+
expect(isOhOperationSizeError(narrow)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
export const OH_OPERATION_SIZE_ERROR_CODE_V1 = "oh.operation-size.v1" as const;
|
|
2
|
+
|
|
3
|
+
const OH_CONFLICT_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhConflictError/v1");
|
|
4
|
+
const OH_DEPENDENCY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhDependencyError/v1");
|
|
5
|
+
const OH_INTEGRITY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhIntegrityError/v1");
|
|
6
|
+
const OH_OPERATION_SIZE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhOperationSizeError/v1");
|
|
7
|
+
const OH_PROFILE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhProfileError/v1");
|
|
8
|
+
|
|
9
|
+
function immutableOwnValue(value: object, key: PropertyKey): unknown {
|
|
10
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
11
|
+
return descriptor !== undefined
|
|
12
|
+
&& descriptor.get === undefined
|
|
13
|
+
&& descriptor.set === undefined
|
|
14
|
+
&& descriptor.configurable === false
|
|
15
|
+
&& descriptor.writable === false
|
|
16
|
+
? descriptor.value
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function brandNativeError(value: Error, brand: symbol): void {
|
|
21
|
+
Object.defineProperty(value, brand, {
|
|
22
|
+
configurable: false,
|
|
23
|
+
enumerable: false,
|
|
24
|
+
value: true,
|
|
25
|
+
writable: false,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasNativeErrorBrand(value: unknown, brand: symbol): boolean {
|
|
30
|
+
try {
|
|
31
|
+
return Error.isError(value) && immutableOwnValue(value, brand) === true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasNativeSubclassInstance(constructor: Function, value: unknown): boolean {
|
|
38
|
+
return Function.prototype[Symbol.hasInstance].call(constructor, value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Recognizes a compare-and-swap conflict across separately bundled Oh entrypoints. */
|
|
42
|
+
export function isOhConflictError(value: unknown): value is OhConflictError {
|
|
43
|
+
return hasNativeErrorBrand(value, OH_CONFLICT_ERROR_BRAND_V1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class OhConflictError extends Error {
|
|
47
|
+
static override [Symbol.hasInstance](value: unknown): boolean {
|
|
48
|
+
return this === OhConflictError
|
|
49
|
+
? isOhConflictError(value)
|
|
50
|
+
: hasNativeSubclassInstance(this, value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
constructor(message: string) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "OhConflictError";
|
|
56
|
+
brandNativeError(this, OH_CONFLICT_ERROR_BRAND_V1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Recognizes an authority-integrity failure across separately bundled Oh entrypoints. */
|
|
61
|
+
export function isOhIntegrityError(value: unknown): value is OhIntegrityError {
|
|
62
|
+
return hasNativeErrorBrand(value, OH_INTEGRITY_ERROR_BRAND_V1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class OhIntegrityError extends Error {
|
|
66
|
+
static override [Symbol.hasInstance](value: unknown): boolean {
|
|
67
|
+
return this === OhIntegrityError
|
|
68
|
+
? isOhIntegrityError(value)
|
|
69
|
+
: hasNativeSubclassInstance(this, value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
constructor(message: string) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "OhIntegrityError";
|
|
75
|
+
brandNativeError(this, OH_INTEGRITY_ERROR_BRAND_V1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Recognizes a missing-dependency failure across separately bundled Oh entrypoints. */
|
|
80
|
+
export function isOhDependencyError(value: unknown): value is OhDependencyError {
|
|
81
|
+
return hasNativeErrorBrand(value, OH_DEPENDENCY_ERROR_BRAND_V1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class OhDependencyError extends Error {
|
|
85
|
+
static override [Symbol.hasInstance](value: unknown): boolean {
|
|
86
|
+
return this === OhDependencyError
|
|
87
|
+
? isOhDependencyError(value)
|
|
88
|
+
: hasNativeSubclassInstance(this, value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
constructor(message: string) {
|
|
92
|
+
super(message);
|
|
93
|
+
this.name = "OhDependencyError";
|
|
94
|
+
brandNativeError(this, OH_DEPENDENCY_ERROR_BRAND_V1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Recognizes a store-profile refusal across separately bundled Oh entrypoints. */
|
|
99
|
+
export function isOhProfileError(value: unknown): value is OhProfileError {
|
|
100
|
+
return hasNativeErrorBrand(value, OH_PROFILE_ERROR_BRAND_V1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class OhProfileError extends Error {
|
|
104
|
+
static override [Symbol.hasInstance](value: unknown): boolean {
|
|
105
|
+
return this === OhProfileError
|
|
106
|
+
? isOhProfileError(value)
|
|
107
|
+
: hasNativeSubclassInstance(this, value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
constructor(message: string) {
|
|
111
|
+
super(message);
|
|
112
|
+
this.name = "OhProfileError";
|
|
113
|
+
brandNativeError(this, OH_PROFILE_ERROR_BRAND_V1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Recognizes this precommit refusal across separately bundled Oh entrypoints.
|
|
119
|
+
* Native Error identity plus immutable branded fields excludes copied plain
|
|
120
|
+
* objects while preserving one stable discriminator for package consumers.
|
|
121
|
+
*/
|
|
122
|
+
export function isOhOperationSizeError(value: unknown): value is OhOperationSizeError {
|
|
123
|
+
try {
|
|
124
|
+
if (!Error.isError(value) || !(value instanceof RangeError)) return false;
|
|
125
|
+
const operationBytes = immutableOwnValue(value, "operationBytes");
|
|
126
|
+
const maximumOperationBytes = immutableOwnValue(value, "maximumOperationBytes");
|
|
127
|
+
return immutableOwnValue(value, OH_OPERATION_SIZE_ERROR_BRAND_V1) === true
|
|
128
|
+
&& immutableOwnValue(value, "code") === OH_OPERATION_SIZE_ERROR_CODE_V1
|
|
129
|
+
&& Number.isSafeInteger(operationBytes)
|
|
130
|
+
&& (operationBytes as number) > 0
|
|
131
|
+
&& Number.isSafeInteger(maximumOperationBytes)
|
|
132
|
+
&& (maximumOperationBytes as number) > 0
|
|
133
|
+
&& (operationBytes as number) > (maximumOperationBytes as number);
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class OhOperationSizeError extends RangeError {
|
|
140
|
+
declare readonly code: typeof OH_OPERATION_SIZE_ERROR_CODE_V1;
|
|
141
|
+
declare readonly maximumOperationBytes: number;
|
|
142
|
+
declare readonly operationBytes: number;
|
|
143
|
+
|
|
144
|
+
static override [Symbol.hasInstance](value: unknown): boolean {
|
|
145
|
+
return this === OhOperationSizeError
|
|
146
|
+
? isOhOperationSizeError(value)
|
|
147
|
+
: hasNativeSubclassInstance(this, value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
constructor(operationBytes: number, maximumOperationBytes: number) {
|
|
151
|
+
if (!Number.isSafeInteger(operationBytes) || operationBytes < 1
|
|
152
|
+
|| !Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1
|
|
153
|
+
|| operationBytes <= maximumOperationBytes) {
|
|
154
|
+
throw new TypeError("Invalid Oh operation size refusal.");
|
|
155
|
+
}
|
|
156
|
+
super(`The ${operationBytes}-byte operation exceeds the host-declared ${maximumOperationBytes}-byte canonical bound.`);
|
|
157
|
+
this.name = "OhOperationSizeError";
|
|
158
|
+
Object.defineProperties(this, {
|
|
159
|
+
[OH_OPERATION_SIZE_ERROR_BRAND_V1]: {
|
|
160
|
+
configurable: false,
|
|
161
|
+
enumerable: false,
|
|
162
|
+
value: true,
|
|
163
|
+
writable: false,
|
|
164
|
+
},
|
|
165
|
+
code: {
|
|
166
|
+
configurable: false,
|
|
167
|
+
enumerable: true,
|
|
168
|
+
value: OH_OPERATION_SIZE_ERROR_CODE_V1,
|
|
169
|
+
writable: false,
|
|
170
|
+
},
|
|
171
|
+
maximumOperationBytes: {
|
|
172
|
+
configurable: false,
|
|
173
|
+
enumerable: true,
|
|
174
|
+
value: maximumOperationBytes,
|
|
175
|
+
writable: false,
|
|
176
|
+
},
|
|
177
|
+
operationBytes: {
|
|
178
|
+
configurable: false,
|
|
179
|
+
enumerable: true,
|
|
180
|
+
value: operationBytes,
|
|
181
|
+
writable: false,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/graph.ts
CHANGED
|
@@ -19,12 +19,12 @@ export const OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
|
19
19
|
recordsPerSnapshot: 65_536,
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
export const OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
22
|
+
export const OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = Object.freeze([
|
|
23
23
|
"activity", "assertion", "context", "dependency-manifest", "edition", "entity",
|
|
24
24
|
"evidence", "identity-operation", "inquiry", "inquiry-event", "review-decision",
|
|
25
25
|
"rights-decision", "schema", "shape", "statement", "type-membership", "view",
|
|
26
26
|
"vocabulary",
|
|
27
|
-
] as const;
|
|
27
|
+
] as const);
|
|
28
28
|
export type KnowledgeGraphRecordKindV1 = (typeof OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1)[number];
|
|
29
29
|
|
|
30
30
|
export type KnowledgeGraphRecordV1 = Readonly<{
|
package/src/libsql.test.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
OH_WORKING_STORE_PROFILE_V1,
|
|
19
19
|
OhConflictError,
|
|
20
20
|
OhIntegrityError,
|
|
21
|
+
OhOperationSizeError,
|
|
21
22
|
OhProfileError,
|
|
22
23
|
OhPurgedSpaceError,
|
|
23
24
|
} from "./store";
|
|
@@ -341,6 +342,41 @@ describe("direct libSQL Oh authority", () => {
|
|
|
341
342
|
client.close();
|
|
342
343
|
});
|
|
343
344
|
|
|
345
|
+
test("honors the host-declared operation byte bound before replay or persistence", async () => {
|
|
346
|
+
const client = await bootstrappedClient();
|
|
347
|
+
const authority = await createOhLibSqlStoreAuthorityV1(client, {
|
|
348
|
+
profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:operation-bound", spaceId: "operation-bound",
|
|
349
|
+
});
|
|
350
|
+
const empty = await authority.store.head();
|
|
351
|
+
const firstChanges = [{ kind: "put" as const,
|
|
352
|
+
record: entity("entity:bounded", "Bounded"), v: 1 as const }];
|
|
353
|
+
const first = await authority.store.commit({ actorId: "agent.bound", changes: firstChanges,
|
|
354
|
+
expectedHead: empty, operationId: "op_bounded_first" });
|
|
355
|
+
|
|
356
|
+
const replayError: unknown = await authority.store.commit({ actorId: "agent.bound", changes: firstChanges,
|
|
357
|
+
expectedHead: empty, maximumOperationBytes: 1,
|
|
358
|
+
operationId: "op_bounded_first" }).catch((error: unknown) => error);
|
|
359
|
+
const commitError: unknown = await authority.store.commit({ actorId: "agent.bound", changes: [{ kind: "put",
|
|
360
|
+
record: entity("entity:rejected", "Rejected"), v: 1 }], expectedHead: await authority.store.head(),
|
|
361
|
+
maximumOperationBytes: 1, operationId: "op_bounded_rejected" }).catch((error: unknown) => error);
|
|
362
|
+
for (const error of [replayError, commitError]) {
|
|
363
|
+
expect(error).toBeInstanceOf(OhOperationSizeError);
|
|
364
|
+
expect(error).toBeInstanceOf(RangeError);
|
|
365
|
+
expect(error).toMatchObject({ maximumOperationBytes: 1,
|
|
366
|
+
operationBytes: expect.any(Number) });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
expect(await authority.store.head()).toMatchObject({
|
|
370
|
+
operationSha256: first.operationSha256,
|
|
371
|
+
sequence: 1,
|
|
372
|
+
});
|
|
373
|
+
expect(client.database.query<{ count: number }, [string]>(`SELECT count(*) AS count
|
|
374
|
+
FROM oh_authority_operations WHERE space_id = ?`).get("operation-bound")?.count).toBe(1);
|
|
375
|
+
expect((await authority.store.snapshot()).records.map(({ key }) => key)).toEqual(["entity:bounded"]);
|
|
376
|
+
await authority.store.close();
|
|
377
|
+
client.close();
|
|
378
|
+
});
|
|
379
|
+
|
|
344
380
|
test("uses compare-and-swap guards to leave no partial remote mutation", async () => {
|
|
345
381
|
const client = await bootstrappedClient();
|
|
346
382
|
const first = await createOhLibSqlStoreAuthorityV1(client, {
|
package/src/libsql.ts
CHANGED
|
@@ -11,7 +11,11 @@ import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
|
|
|
11
11
|
import { canonicalKnowledgeGraphChangesV1, knowledgeGraphRecordRefV1, OH_GRAPH_LIMITS_V1,
|
|
12
12
|
parseKnowledgeGraphRecordV1,
|
|
13
13
|
type KnowledgeGraphRecordV1 } from "./graph";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
OH_OPERATION_MAX_BYTES_V1,
|
|
16
|
+
parseOhOperationV1,
|
|
17
|
+
type OhOperationV1,
|
|
18
|
+
} from "./operation";
|
|
15
19
|
import {
|
|
16
20
|
createOhDependencyClosureV1,
|
|
17
21
|
createOhSpacePurgeReceiptV1,
|
|
@@ -21,6 +25,7 @@ import {
|
|
|
21
25
|
OH_WORKING_STORE_PROFILE_V1,
|
|
22
26
|
OhConflictError,
|
|
23
27
|
OhIntegrityError,
|
|
28
|
+
OhOperationSizeError,
|
|
24
29
|
OhProfileError,
|
|
25
30
|
OhPurgedSpaceError,
|
|
26
31
|
parseOhHeadRefV1,
|
|
@@ -1261,8 +1266,14 @@ class OhLibSqlStoreV1 implements OhStoreV1 {
|
|
|
1261
1266
|
this.#assertOpen();
|
|
1262
1267
|
const actorId = safeCode(input.actorId);
|
|
1263
1268
|
const operationId = safeCode(input.operationId);
|
|
1269
|
+
const maximumOperationBytes = input.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
|
|
1264
1270
|
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
1265
|
-
if (actorId === null || operationId === null || changes.length === 0
|
|
1271
|
+
if (actorId === null || operationId === null || changes.length === 0
|
|
1272
|
+
|| !Number.isSafeInteger(maximumOperationBytes)
|
|
1273
|
+
|| maximumOperationBytes < 1
|
|
1274
|
+
|| maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
|
|
1275
|
+
throw new TypeError("Invalid Oh commit input.");
|
|
1276
|
+
}
|
|
1266
1277
|
if (changes.length > OH_LIBSQL_STORE_LIMITS_V1.changesPerCommit) {
|
|
1267
1278
|
throw new RangeError("A direct libSQL commit exceeds its change-count bound.");
|
|
1268
1279
|
}
|
|
@@ -1277,6 +1288,10 @@ class OhLibSqlStoreV1 implements OhStoreV1 {
|
|
|
1277
1288
|
if (duplicate.actorId !== actorId || canonicalJson(duplicate.changes) !== canonicalJson(changes)) {
|
|
1278
1289
|
throw new OhConflictError("The operation ID is already bound to different content.");
|
|
1279
1290
|
}
|
|
1291
|
+
const operationBytes = utf8ByteLength(canonicalJson(duplicate));
|
|
1292
|
+
if (operationBytes > maximumOperationBytes) {
|
|
1293
|
+
throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
|
|
1294
|
+
}
|
|
1280
1295
|
return duplicate;
|
|
1281
1296
|
}
|
|
1282
1297
|
if (!Number.isSafeInteger(input.expectedHead.generation) || input.expectedHead.generation < 0
|
|
@@ -1286,9 +1301,15 @@ class OhLibSqlStoreV1 implements OhStoreV1 {
|
|
|
1286
1301
|
}
|
|
1287
1302
|
const snapshot = await this.#currentMaterializedSnapshot(current, OH_GRAPH_LIMITS_V1.recordsPerSnapshot);
|
|
1288
1303
|
const transition = transitionOhSnapshotV1({ actorId, changes,
|
|
1289
|
-
instant: input.instant ?? canonicalNow(),
|
|
1304
|
+
instant: input.instant ?? canonicalNow(), maximumOperationBytes,
|
|
1305
|
+
operationId, snapshot, spaceId: this.binding.spaceId });
|
|
1290
1306
|
const operation = transition.operation;
|
|
1291
|
-
|
|
1307
|
+
const operationJson = canonicalJson(operation);
|
|
1308
|
+
const operationBytes = utf8ByteLength(operationJson);
|
|
1309
|
+
if (operationBytes > maximumOperationBytes) {
|
|
1310
|
+
throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
|
|
1311
|
+
}
|
|
1312
|
+
if (operationBytes > OH_LIBSQL_STORE_LIMITS_V1.operationBytes) {
|
|
1292
1313
|
throw new RangeError("A direct libSQL operation exceeds its canonical byte bound.");
|
|
1293
1314
|
}
|
|
1294
1315
|
const existsOperation = "EXISTS (SELECT 1 FROM oh_authority_operations WHERE operation_sha256 = ?)";
|
|
@@ -1301,7 +1322,7 @@ class OhLibSqlStoreV1 implements OhStoreV1 {
|
|
|
1301
1322
|
WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?)`,
|
|
1302
1323
|
args: [operation.operationSha256, this.binding.spaceId, operation.sequence, operation.operationId,
|
|
1303
1324
|
operation.parentOperationSha256, operation.graphRevisionSha256, operation.recordsSha256,
|
|
1304
|
-
|
|
1325
|
+
operationJson, operation.instant, this.binding.spaceId, current.generation,
|
|
1305
1326
|
current.operationSha256],
|
|
1306
1327
|
}];
|
|
1307
1328
|
for (const [ordinal, change] of operation.changes.entries()) {
|