@hraness/oh 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +3 -0
- package/README.md +12 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15590 -1705
- package/dist/index.js +15209 -261
- package/dist/libsql-model.d.ts +75 -0
- package/dist/libsql-model.d.ts.map +1 -0
- package/dist/libsql-platform.d.ts +24 -0
- package/dist/libsql-platform.d.ts.map +1 -0
- package/dist/libsql-program.d.ts +64 -0
- package/dist/libsql-program.d.ts.map +1 -0
- package/dist/libsql-runtime.d.ts +13 -0
- package/dist/libsql-runtime.d.ts.map +1 -0
- package/dist/libsql.d.ts +4 -36
- package/dist/libsql.d.ts.map +1 -1
- package/dist/libsql.js +16583 -1426
- package/dist/memory-authority-platform.d.ts +210 -0
- package/dist/memory-authority-platform.d.ts.map +1 -0
- package/dist/memory-authority-program.d.ts +15 -0
- package/dist/memory-authority-program.d.ts.map +1 -0
- package/dist/memory-authority-runtime.d.ts +4 -0
- package/dist/memory-authority-runtime.d.ts.map +1 -0
- package/dist/memory-core.d.ts +522 -0
- package/dist/memory-core.d.ts.map +1 -0
- package/dist/memory.d.ts +5 -445
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +18232 -4143
- package/dist/sdk.js +15266 -318
- package/dist/semantic-cloud.js +1 -260
- package/dist/semantic-model.d.ts +75 -0
- package/dist/semantic-model.d.ts.map +1 -0
- package/dist/semantic-platform.d.ts +32 -0
- package/dist/semantic-platform.d.ts.map +1 -0
- package/dist/semantic-program.d.ts +18 -0
- package/dist/semantic-program.d.ts.map +1 -0
- package/dist/semantic-runtime.d.ts +6 -0
- package/dist/semantic-runtime.d.ts.map +1 -0
- package/dist/semantic.d.ts +6 -57
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +15541 -514
- package/dist/sqlite/index.js +1148 -1378
- package/dist/sqlite/port.d.ts +1 -1
- package/dist/sqlite/port.d.ts.map +1 -1
- package/dist/sync-libsql-program.d.ts +5 -0
- package/dist/sync-libsql-program.d.ts.map +1 -0
- package/dist/sync-model.d.ts +60 -0
- package/dist/sync-model.d.ts.map +1 -0
- package/dist/sync-platform.d.ts +41 -0
- package/dist/sync-platform.d.ts.map +1 -0
- package/dist/sync-program.d.ts +11 -0
- package/dist/sync-program.d.ts.map +1 -0
- package/dist/sync-runtime.d.ts +6 -0
- package/dist/sync-runtime.d.ts.map +1 -0
- package/dist/sync.d.ts +5 -55
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +15174 -226
- package/package.json +7 -5
- package/skills/oh/SKILL.md +3 -2
- package/spec/v1/store.md +14 -0
- package/src/cli.test.ts +35 -0
- package/src/cli.ts +4 -3
- package/src/cloudflare-embedding.ts +1 -1
- package/src/libsql-lifecycle.test.ts +76 -0
- package/src/libsql-model.ts +350 -0
- package/src/libsql-platform.ts +51 -0
- package/src/libsql-program.ts +1583 -0
- package/src/libsql-runtime.ts +89 -0
- package/src/libsql-semantic-v2.ts +1 -1
- package/src/libsql-semantic.ts +1 -1
- package/src/libsql.test.ts +245 -0
- package/src/libsql.ts +26 -1698
- package/src/memory-authority-platform.ts +133 -0
- package/src/memory-authority-program.ts +309 -0
- package/src/memory-authority-runtime.ts +40 -0
- package/src/memory-core.ts +2401 -0
- package/src/memory.test.ts +55 -0
- package/src/memory.ts +68 -2759
- package/src/semantic-model.ts +142 -0
- package/src/semantic-platform.ts +130 -0
- package/src/semantic-program.ts +113 -0
- package/src/semantic-runtime.ts +53 -0
- package/src/semantic.test.ts +115 -1
- package/src/semantic.ts +14 -324
- package/src/sqlite/port.ts +1 -1
- package/src/sync-libsql-program.ts +170 -0
- package/src/sync-lifecycle.test.ts +110 -0
- package/src/sync-model.ts +530 -0
- package/src/sync-platform.ts +87 -0
- package/src/sync-program.ts +117 -0
- package/src/sync-runtime.ts +33 -0
- package/src/sync.ts +10 -765
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/oh",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "open-source tools for agentic research",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
},
|
|
102
102
|
"scripts": {
|
|
103
103
|
"build": "bun run build:js && bun run build:portable && bun run build:types",
|
|
104
|
-
"build:js": "bun build ./src/index.ts ./src/sdk.ts ./src/sqlite/index.ts ./src/sync.ts ./src/
|
|
105
|
-
"build:portable": "bun build ./src/store.ts ./src/
|
|
104
|
+
"build:js": "bun build ./src/index.ts ./src/sdk.ts ./src/sqlite/index.ts ./src/sync.ts --outdir ./dist --target bun --format esm --external bun:sqlite && bun build ./src/cli.ts --outdir ./dist --target bun --format esm --external bun:sqlite && bun build ./src/semantic.ts --outdir ./dist --target bun --format esm --external bun:sqlite",
|
|
105
|
+
"build:portable": "bun build ./src/store.ts ./src/projection-public.ts ./src/projection-suss.ts ./src/memory-page.ts ./src/semantic-cloud.ts --outdir ./dist --target node --format esm --external @suss/datalog && bun build ./src/libsql.ts --outdir ./dist --target node --format esm --external @suss/datalog && bun build ./src/memory.ts --outdir ./dist --target node --format esm --external @suss/datalog",
|
|
106
106
|
"build:types": "tsc -p tsconfig.build.json",
|
|
107
|
-
"check": "bun run typecheck && bun run typecheck:scripts && bun run test && bun run build && bun run test:types:node && bun run test:node && bun run test:node-projection && bun run test:package",
|
|
107
|
+
"check": "bun run typecheck && bun run typecheck:scripts && bun run check:effect && bun run test && bun run build && bun run test:types:node && bun run test:node && bun run test:node-projection && bun run test:package",
|
|
108
108
|
"test:node": "node ./tests/node-portable.mjs",
|
|
109
109
|
"test:types:node": "tsc -p tsconfig.node-portable.json --noEmit",
|
|
110
110
|
"test:package": "bun run ./scripts/package-smoke.ts",
|
|
@@ -112,11 +112,13 @@
|
|
|
112
112
|
"test": "bun test ./src ./tests ./site/tests/source.test.ts",
|
|
113
113
|
"test:node-projection": "node --test ./scripts/projection-node.test.mjs",
|
|
114
114
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
115
|
-
"typecheck:scripts": "tsc -p tsconfig.scripts.json --noEmit"
|
|
115
|
+
"typecheck:scripts": "tsc -p tsconfig.scripts.json --noEmit",
|
|
116
|
+
"check:effect": "bun run ./scripts/check-effect-architecture.ts && bun test ./scripts/effect-architecture.test.ts"
|
|
116
117
|
},
|
|
117
118
|
"devDependencies": {
|
|
118
119
|
"@suss/datalog": "0.20.0",
|
|
119
120
|
"@types/bun": "1.3.14",
|
|
121
|
+
"effect": "3.22.1",
|
|
120
122
|
"sigstore": "4.1.1",
|
|
121
123
|
"typescript": "5.9.3"
|
|
122
124
|
},
|
package/skills/oh/SKILL.md
CHANGED
|
@@ -30,8 +30,9 @@ oh --help
|
|
|
30
30
|
oh version
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
This source tree prepares version `0.4.2`. Use the verified public CLI
|
|
34
|
+
`@hraness/oh@0.4.1` until the next release completes publication. Its identical
|
|
35
|
+
tarball and checksum are mirrored by the immutable GitHub Release `v0.4.1`.
|
|
35
36
|
It requires Bun 1.3.14 or newer. The versioned contract is published at
|
|
36
37
|
<https://oh.computer/spec/>.
|
|
37
38
|
|
package/spec/v1/store.md
CHANGED
|
@@ -149,3 +149,17 @@ Operation-record deletion resolves ownership through the canonical operation;
|
|
|
149
149
|
the purge postcondition also rejects any global orphan or cross-space owner
|
|
150
150
|
mismatch. Purge receipts are immutable and intentionally retain only binding,
|
|
151
151
|
prior-head, and purge-event evidence.
|
|
152
|
+
|
|
153
|
+
### Direct libSQL shutdown
|
|
154
|
+
|
|
155
|
+
Calling an authority store's `close()` immediately fences new calls on both its
|
|
156
|
+
store and host handles. Already admitted operations finish their native calls,
|
|
157
|
+
transaction result validation and reconciliation before close resolves. Concurrent
|
|
158
|
+
commits remain concurrent and are arbitrated by SQL compare-and-swap.
|
|
159
|
+
|
|
160
|
+
The client is borrowed unless `closeClient: true` was selected when opening the
|
|
161
|
+
authority. An owned client is closed once after the drain; repeated `close()` calls
|
|
162
|
+
observe that same result. A failed operation and a failed authority close reject
|
|
163
|
+
their respective Promises with their original values. The standalone
|
|
164
|
+
`purgeOhLibSqlWorkingSpaceV1` helper retains its `finally` contract: when both purge
|
|
165
|
+
and its requested client close fail, the close failure is its rejection value.
|
package/src/cli.test.ts
CHANGED
|
@@ -24,6 +24,41 @@ async function run(arguments_: readonly string[], cwd = REPOSITORY_ROOT): Promis
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
describe("oh CLI", () => {
|
|
27
|
+
test("loads the SDK only after a store command passes validation", async () => {
|
|
28
|
+
const root = await mkdtemp(join(tmpdir(), "oh-cli-lazy-test-"));
|
|
29
|
+
roots.push(root);
|
|
30
|
+
const probe = `
|
|
31
|
+
import { createRequire } from "node:module";
|
|
32
|
+
import { dirname, sep } from "node:path";
|
|
33
|
+
const require = createRequire(${JSON.stringify(CLI_PATH)});
|
|
34
|
+
const effectRoot = dirname(require.resolve("effect/package.json")) + sep;
|
|
35
|
+
const loaded = () => Object.keys(require.cache).some((path) => path.startsWith(effectRoot));
|
|
36
|
+
const { runOhCli } = await import(${JSON.stringify(CLI_PATH)});
|
|
37
|
+
if (loaded()) throw new Error("CLI import loaded Effect");
|
|
38
|
+
for (const args of [["help"], ["version"], ["contract"]]) {
|
|
39
|
+
if (await runOhCli(args) !== 0) throw new Error("Static command failed");
|
|
40
|
+
if (loaded()) throw new Error("Static command loaded Effect");
|
|
41
|
+
}
|
|
42
|
+
let rejected = false;
|
|
43
|
+
try { await runOhCli(["sync", "export", "--limit", "0"]); }
|
|
44
|
+
catch (error) { if (!(error instanceof TypeError)) throw error; rejected = true; }
|
|
45
|
+
if (!rejected || loaded()) throw new Error("Invalid command crossed the SDK boundary");
|
|
46
|
+
// A real store command is also the positive control for the cache observer.
|
|
47
|
+
if (await runOhCli(["init"]) !== 0 || !loaded()) {
|
|
48
|
+
throw new Error("Store command did not load the runtime");
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
const child = Bun.spawn([process.execPath, "--eval", probe], {
|
|
52
|
+
cwd: root, stderr: "pipe", stdout: "pipe",
|
|
53
|
+
});
|
|
54
|
+
const [code, stderr, stdout] = await Promise.all([
|
|
55
|
+
child.exited, new Response(child.stderr).text(), new Response(child.stdout).text(),
|
|
56
|
+
]);
|
|
57
|
+
expect({ code, stderr }).toEqual({ code: 0, stderr: "" });
|
|
58
|
+
expect(stdout).toContain('"spaceId":"default"');
|
|
59
|
+
expect(existsSync(join(root, ".oh", "oh.sqlite"))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
27
62
|
test("initializes, writes, searches, and verifies one local database", async () => {
|
|
28
63
|
const root = await mkdtemp(join(tmpdir(), "oh-cli-test-"));
|
|
29
64
|
roots.push(root);
|
package/src/cli.ts
CHANGED
|
@@ -5,11 +5,10 @@ import { canonicalJson, opaqueId, safeCode, type JsonValue } from "./canonical";
|
|
|
5
5
|
import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
|
|
6
6
|
import { OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, createKnowledgeGraphRecordV1,
|
|
7
7
|
type KnowledgeGraphRecordKindV1, type KnowledgeGraphRecordV1 } from "./graph";
|
|
8
|
-
import { Oh } from "./sdk";
|
|
9
8
|
import { OH_SQLITE_SCHEMA_VERSION } from "./sqlite/migrations";
|
|
10
|
-
import { createOhSyncBundleV1, OH_SYNC_BUNDLE_MAX_BYTES_V1, parseOhSyncBundleV1 } from "./sync";
|
|
9
|
+
import { createOhSyncBundleV1, OH_SYNC_BUNDLE_MAX_BYTES_V1, parseOhSyncBundleV1 } from "./sync-model";
|
|
11
10
|
|
|
12
|
-
export const OH_PACKAGE_VERSION = "0.4.
|
|
11
|
+
export const OH_PACKAGE_VERSION = "0.4.2" as const;
|
|
13
12
|
|
|
14
13
|
type ParsedArguments = { options: Map<string, string[]>; positionals: string[] };
|
|
15
14
|
type ValidatedInvocation = Readonly<{
|
|
@@ -244,6 +243,8 @@ export async function runOhCli(arguments_: readonly string[]): Promise<number> {
|
|
|
244
243
|
print({ manifest: OH_CONTRACT_MANIFEST_V1, sqliteSchemaVersion: OH_SQLITE_SCHEMA_VERSION, v: 1 });
|
|
245
244
|
return 0;
|
|
246
245
|
}
|
|
246
|
+
// Static commands and invalid input need no store or sync runtime.
|
|
247
|
+
const { Oh } = await import("./sdk");
|
|
247
248
|
const oh = Oh.open({ databasePath: validated.databasePath, spaceId: validated.spaceId });
|
|
248
249
|
try {
|
|
249
250
|
if (command === "init") { print({ head: oh.head(), spaceId: oh.store.spaceId, v: 1 }); return 0; }
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
utf8ByteLength,
|
|
7
7
|
type Sha256Hex,
|
|
8
8
|
} from "./canonical";
|
|
9
|
-
import { normalizeOhEmbeddingV1 } from "./semantic";
|
|
9
|
+
import { normalizeOhEmbeddingV1 } from "./semantic-model";
|
|
10
10
|
|
|
11
11
|
const cloudflareEmbeddingProfilePayload = Object.freeze({
|
|
12
12
|
dimensions: 768,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { Cause, Effect, Exit, Fiber } from "effect";
|
|
3
|
+
import { LibSqlStoreProgram, makeLibSqlOwner } from "./libsql-program";
|
|
4
|
+
import { LibSqlAuthorityClient, libSqlAuthorityClientLive } from "./libsql-platform";
|
|
5
|
+
import { createOhStoreBindingV1, emptyOhHeadV1, OH_CANONICAL_STORE_PROFILE_V1 } from "./store";
|
|
6
|
+
|
|
7
|
+
test("interrupting an admitted operation cannot complete drain before the native transaction settles", async () => {
|
|
8
|
+
let entered!: () => void;
|
|
9
|
+
const started = new Promise<void>(resolve => { entered = resolve; });
|
|
10
|
+
let settle!: () => void;
|
|
11
|
+
const held = new Promise<void>(resolve => { settle = resolve; });
|
|
12
|
+
const events: string[] = [];
|
|
13
|
+
const owner = Effect.runSync(makeLibSqlOwner);
|
|
14
|
+
expect(Effect.runSync(owner.admit)).toBe(true);
|
|
15
|
+
const operation = Effect.gen(function* () {
|
|
16
|
+
const client = yield* LibSqlAuthorityClient;
|
|
17
|
+
yield* client.batch([{ sql: "native transaction" }], "write");
|
|
18
|
+
yield* Effect.sync(() => { events.push("reconciled"); });
|
|
19
|
+
}).pipe(Effect.provide(libSqlAuthorityClientLive({
|
|
20
|
+
execute: async () => ({ rows: [] }),
|
|
21
|
+
batch: async () => {
|
|
22
|
+
entered();
|
|
23
|
+
await held;
|
|
24
|
+
events.push("native-settled");
|
|
25
|
+
return [{ rows: [] }];
|
|
26
|
+
},
|
|
27
|
+
})));
|
|
28
|
+
const fiber = Effect.runFork(owner.complete(operation));
|
|
29
|
+
await started;
|
|
30
|
+
Effect.runSync(owner.beginClose);
|
|
31
|
+
expect(Effect.runSync(owner.admit)).toBe(false);
|
|
32
|
+
let drained = false;
|
|
33
|
+
const drain = Effect.runPromise(owner.drained).then(() => { drained = true; });
|
|
34
|
+
const interruption = Effect.runPromise(Fiber.interrupt(fiber));
|
|
35
|
+
await Promise.resolve();
|
|
36
|
+
expect(drained).toBe(false);
|
|
37
|
+
expect(events).toEqual([]);
|
|
38
|
+
settle();
|
|
39
|
+
const exit = await interruption;
|
|
40
|
+
await drain;
|
|
41
|
+
expect(Exit.isFailure(exit) && Cause.isInterrupted(exit.cause)).toBe(true);
|
|
42
|
+
expect(events).toEqual(["native-settled", "reconciled"]);
|
|
43
|
+
expect(drained).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
test("expected row and input failures stay typed while an unrelated response defect remains a defect", async () => {
|
|
48
|
+
const binding = createOhStoreBindingV1({ profile: OH_CANONICAL_STORE_PROFILE_V1,
|
|
49
|
+
realmId: "realm:typed-failures", spaceId: "typed-failures", v: 1 });
|
|
50
|
+
const program = new LibSqlStoreProgram({
|
|
51
|
+
execute: () => Effect.succeed({ rows: [{}] }),
|
|
52
|
+
batch: () => Effect.succeed([]), close: Effect.void, currentInstant: Effect.succeed("2026-09-06T12:00:00.000Z"),
|
|
53
|
+
}, binding);
|
|
54
|
+
for (const [operation, tag] of [
|
|
55
|
+
[program.head().pipe(Effect.asVoid), "LibSqlIntegrity"],
|
|
56
|
+
[program.commit({ actorId: "", operationId: "", changes: [], expectedHead: emptyOhHeadV1() }).pipe(Effect.asVoid), "LibSqlValidation"],
|
|
57
|
+
] as const) {
|
|
58
|
+
const exit = await Effect.runPromiseExit(operation);
|
|
59
|
+
expect(Exit.isFailure(exit)).toBe(true);
|
|
60
|
+
if (Exit.isFailure(exit)) {
|
|
61
|
+
expect(Array.from(Cause.defects(exit.cause))).toEqual([]);
|
|
62
|
+
expect(Array.from(Cause.failures(exit.cause)).map(failure => failure._tag)).toEqual([tag]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const defect = Object.freeze({ kind: "unexpected-provider-getter" });
|
|
66
|
+
const defective = new LibSqlStoreProgram({
|
|
67
|
+
execute: () => Effect.succeed({ get rows(): [] { throw defect; } }),
|
|
68
|
+
batch: () => Effect.succeed([]), close: Effect.void, currentInstant: Effect.succeed("2026-09-06T12:00:00.000Z"),
|
|
69
|
+
}, binding);
|
|
70
|
+
const exit = await Effect.runPromiseExit(defective.head());
|
|
71
|
+
expect(Exit.isFailure(exit)).toBe(true);
|
|
72
|
+
if (Exit.isFailure(exit)) {
|
|
73
|
+
expect(Array.from(Cause.failures(exit.cause))).toEqual([]);
|
|
74
|
+
expect(Array.from(Cause.defects(exit.cause))).toEqual([defect]);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { canonicalJson, canonicalSha256, parseSha256Hex, type Sha256Hex } from "./canonical";
|
|
2
|
+
|
|
3
|
+
import { parseOhOperationV1, type OhOperationV1 } from "./operation";
|
|
4
|
+
import {
|
|
5
|
+
OhIntegrityError,
|
|
6
|
+
parseOhSpacePurgeReceiptV1,
|
|
7
|
+
parseOhStoreBindingV1,
|
|
8
|
+
type OhHeadV1,
|
|
9
|
+
type OhSpacePurgeReceiptV1,
|
|
10
|
+
type OhStoreBindingV1,
|
|
11
|
+
type OhStoreProfileV1,
|
|
12
|
+
} from "./store";
|
|
13
|
+
|
|
14
|
+
export type OhLibSqlValueV1 = ArrayBuffer | Date | Uint8Array | bigint | boolean | null | number | string;
|
|
15
|
+
export type OhLibSqlStatementV1 = Readonly<{ args?: readonly OhLibSqlValueV1[]; sql: string }>;
|
|
16
|
+
export type OhLibSqlResultV1 = Readonly<{
|
|
17
|
+
rows: readonly (Readonly<Record<string, unknown>> | readonly unknown[])[];
|
|
18
|
+
rowsAffected?: number;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
/** Structural subset implemented by `@libsql/client` clients. */
|
|
22
|
+
export interface OhLibSqlClientV1 {
|
|
23
|
+
batch(
|
|
24
|
+
statements: readonly OhLibSqlStatementV1[],
|
|
25
|
+
mode?: "deferred" | "read" | "write",
|
|
26
|
+
): Promise<readonly OhLibSqlResultV1[]>;
|
|
27
|
+
close?(): void;
|
|
28
|
+
execute(statement: OhLibSqlStatementV1 | string): Promise<OhLibSqlResultV1>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type OhLibSqlStoreAuthorityOptionsV1 = Readonly<{
|
|
32
|
+
closeClient?: boolean;
|
|
33
|
+
profile?: OhStoreProfileV1;
|
|
34
|
+
realmId?: string;
|
|
35
|
+
spaceId?: string;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
export const OH_LIBSQL_STORE_LIMITS_V1 = Object.freeze({
|
|
39
|
+
changesPerCommit: 64,
|
|
40
|
+
changeFeedLimit: 7,
|
|
41
|
+
dependenciesPerCommit: 512,
|
|
42
|
+
historyBytes: 4 * 1024 * 1024,
|
|
43
|
+
historyOperations: 16_384,
|
|
44
|
+
operationBytes: 512 * 1024,
|
|
45
|
+
providerResponseBytes: 9_000_000,
|
|
46
|
+
snapshotComponentBytes: 6 * 1024 * 1024,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const AUTHORITY_SCHEMA_NAME = "oh.libsql-authority.v1";
|
|
50
|
+
export const AUTHORITY_SCHEMA_VERSION = 1;
|
|
51
|
+
export const EMPTY_RECORDS_SHA256 = canonicalSha256([]);
|
|
52
|
+
export const PURGE_ROW_SELECT = `SELECT space_id, binding_sha256, prior_operation_sha256,
|
|
53
|
+
prior_sequence, purged_at, receipt_sha256, receipt_json
|
|
54
|
+
FROM oh_authority_purges WHERE space_id = ?`;
|
|
55
|
+
export const BINDING_ROW_SELECT = `SELECT space_id, realm_id, profile_id, profile_kind,
|
|
56
|
+
profile_sha256, binding_sha256, binding_json FROM oh_authority_bindings WHERE space_id = ?`;
|
|
57
|
+
export const SPACE_PURGE_PROOF_SELECT = `SELECT generation, graph_revision_sha256, head_operation_sha256,
|
|
58
|
+
records_sha256, sequence, contract_id FROM oh_authority_spaces WHERE space_id = ?`;
|
|
59
|
+
export const OPERATION_ROW_COLUMNS = `operation_sha256, space_id, sequence, operation_id,
|
|
60
|
+
parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`;
|
|
61
|
+
// libSQL serializes text again in its JSON response. Twice the UTF-8 text plus all
|
|
62
|
+
// duplicated columns and a fixed row reserve is a conservative upper bound for
|
|
63
|
+
// canonical JSON, whose own escapes can only be escaped once more by transport.
|
|
64
|
+
export const OPERATION_RESPONSE_BYTES = `2 * length(CAST(operation.operation_json AS BLOB))
|
|
65
|
+
+ 2 * (length(operation.operation_sha256) + length(operation.space_id)
|
|
66
|
+
+ length(operation.operation_id) + coalesce(length(operation.parent_operation_sha256), 0)
|
|
67
|
+
+ length(operation.graph_revision_sha256) + length(operation.records_sha256)
|
|
68
|
+
+ length(operation.instant)) + 512`;
|
|
69
|
+
export const RECORD_RESPONSE_BYTES = `2 * length(CAST(record.record_json AS BLOB))
|
|
70
|
+
+ 2 * (length(record.record_key) + length(record.kind) + length(record.record_sha256)
|
|
71
|
+
+ length(record.operation_sha256)) + 384`;
|
|
72
|
+
export const DEPENDENCY_RESPONSE_BYTES = `2 * (length(dependency.record_key)
|
|
73
|
+
+ length(dependency.dependency_key)) + 192`;
|
|
74
|
+
export const OPERATION_RECORD_RESPONSE_BYTES = `2 * (length(materialized.space_id)
|
|
75
|
+
+ length(materialized.operation_sha256) + length(materialized.record_key)
|
|
76
|
+
+ length(materialized.change_kind) + length(materialized.record_sha256)) + 320`;
|
|
77
|
+
|
|
78
|
+
export const AUTHORITY_SCHEMA_TABLE_STATEMENT = `CREATE TABLE IF NOT EXISTS oh_authority_schemas (
|
|
79
|
+
version INTEGER PRIMARY KEY,
|
|
80
|
+
name TEXT NOT NULL UNIQUE,
|
|
81
|
+
schema_sha256 TEXT NOT NULL,
|
|
82
|
+
applied_at TEXT NOT NULL
|
|
83
|
+
) STRICT`;
|
|
84
|
+
|
|
85
|
+
export const AUTHORITY_SCHEMA_STATEMENTS = Object.freeze([
|
|
86
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_contracts (
|
|
87
|
+
contract_id TEXT PRIMARY KEY,
|
|
88
|
+
contract_sha256 TEXT NOT NULL,
|
|
89
|
+
manifest_json TEXT NOT NULL
|
|
90
|
+
) STRICT`,
|
|
91
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_spaces (
|
|
92
|
+
space_id TEXT PRIMARY KEY,
|
|
93
|
+
contract_id TEXT NOT NULL,
|
|
94
|
+
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
95
|
+
head_operation_sha256 TEXT,
|
|
96
|
+
graph_revision_sha256 TEXT,
|
|
97
|
+
records_sha256 TEXT NOT NULL,
|
|
98
|
+
sequence INTEGER NOT NULL CHECK(sequence >= 0),
|
|
99
|
+
created_at TEXT NOT NULL,
|
|
100
|
+
updated_at TEXT NOT NULL,
|
|
101
|
+
CHECK(generation = sequence)
|
|
102
|
+
) STRICT`,
|
|
103
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_operations (
|
|
104
|
+
operation_sha256 TEXT PRIMARY KEY,
|
|
105
|
+
space_id TEXT NOT NULL,
|
|
106
|
+
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
107
|
+
operation_id TEXT NOT NULL,
|
|
108
|
+
parent_operation_sha256 TEXT,
|
|
109
|
+
graph_revision_sha256 TEXT NOT NULL,
|
|
110
|
+
records_sha256 TEXT NOT NULL,
|
|
111
|
+
operation_json TEXT NOT NULL,
|
|
112
|
+
instant TEXT NOT NULL,
|
|
113
|
+
UNIQUE(space_id, sequence),
|
|
114
|
+
UNIQUE(space_id, operation_id)
|
|
115
|
+
) STRICT`,
|
|
116
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_operation_records (
|
|
117
|
+
space_id TEXT NOT NULL,
|
|
118
|
+
operation_sha256 TEXT NOT NULL,
|
|
119
|
+
ordinal INTEGER NOT NULL CHECK(ordinal >= 0),
|
|
120
|
+
record_key TEXT NOT NULL,
|
|
121
|
+
change_kind TEXT NOT NULL CHECK(change_kind IN ('put', 'tombstone')),
|
|
122
|
+
record_sha256 TEXT NOT NULL,
|
|
123
|
+
PRIMARY KEY(operation_sha256, ordinal),
|
|
124
|
+
UNIQUE(operation_sha256, record_key)
|
|
125
|
+
) STRICT`,
|
|
126
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_records (
|
|
127
|
+
space_id TEXT NOT NULL,
|
|
128
|
+
record_key TEXT NOT NULL,
|
|
129
|
+
kind TEXT NOT NULL,
|
|
130
|
+
record_sha256 TEXT NOT NULL,
|
|
131
|
+
record_json TEXT NOT NULL,
|
|
132
|
+
operation_sha256 TEXT NOT NULL,
|
|
133
|
+
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
134
|
+
PRIMARY KEY(space_id, record_key)
|
|
135
|
+
) STRICT`,
|
|
136
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_dependencies (
|
|
137
|
+
space_id TEXT NOT NULL,
|
|
138
|
+
record_key TEXT NOT NULL,
|
|
139
|
+
dependency_key TEXT NOT NULL,
|
|
140
|
+
PRIMARY KEY(space_id, record_key, dependency_key)
|
|
141
|
+
) STRICT`,
|
|
142
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_bindings (
|
|
143
|
+
space_id TEXT PRIMARY KEY,
|
|
144
|
+
realm_id TEXT NOT NULL,
|
|
145
|
+
profile_id TEXT NOT NULL,
|
|
146
|
+
profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')),
|
|
147
|
+
profile_sha256 TEXT NOT NULL,
|
|
148
|
+
binding_sha256 TEXT NOT NULL UNIQUE,
|
|
149
|
+
binding_json TEXT NOT NULL,
|
|
150
|
+
created_at TEXT NOT NULL
|
|
151
|
+
) STRICT`,
|
|
152
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_purges (
|
|
153
|
+
space_id TEXT PRIMARY KEY,
|
|
154
|
+
binding_sha256 TEXT NOT NULL,
|
|
155
|
+
prior_operation_sha256 TEXT,
|
|
156
|
+
prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0),
|
|
157
|
+
purged_at TEXT NOT NULL,
|
|
158
|
+
receipt_sha256 TEXT NOT NULL UNIQUE,
|
|
159
|
+
receipt_json TEXT NOT NULL
|
|
160
|
+
) STRICT`,
|
|
161
|
+
`CREATE TABLE IF NOT EXISTS oh_authority_commit_guards (
|
|
162
|
+
value TEXT NOT NULL CHECK(value = 'ok')
|
|
163
|
+
) STRICT`,
|
|
164
|
+
"CREATE INDEX IF NOT EXISTS oh_authority_operations_space_sequence ON oh_authority_operations(space_id, sequence)",
|
|
165
|
+
"CREATE INDEX IF NOT EXISTS oh_authority_records_space_kind ON oh_authority_records(space_id, kind, record_key)",
|
|
166
|
+
"CREATE INDEX IF NOT EXISTS oh_authority_dependencies_dependency ON oh_authority_dependencies(space_id, dependency_key)",
|
|
167
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operations_no_update
|
|
168
|
+
BEFORE UPDATE ON oh_authority_operations
|
|
169
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operations are immutable'); END`,
|
|
170
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operations_guard_delete
|
|
171
|
+
BEFORE DELETE ON oh_authority_operations
|
|
172
|
+
WHEN NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = OLD.space_id)
|
|
173
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operations require a purge receipt'); END`,
|
|
174
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_no_update
|
|
175
|
+
BEFORE UPDATE ON oh_authority_operation_records
|
|
176
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operation records are immutable'); END`,
|
|
177
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_insert
|
|
178
|
+
BEFORE INSERT ON oh_authority_operation_records
|
|
179
|
+
WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations
|
|
180
|
+
WHERE operation_sha256 = NEW.operation_sha256 AND space_id = NEW.space_id)
|
|
181
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operation record has no owning operation'); END`,
|
|
182
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_delete
|
|
183
|
+
BEFORE DELETE ON oh_authority_operation_records
|
|
184
|
+
WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations AS operation
|
|
185
|
+
JOIN oh_authority_purges AS purge ON purge.space_id = operation.space_id
|
|
186
|
+
WHERE operation.operation_sha256 = OLD.operation_sha256)
|
|
187
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority operation records require a purge receipt'); END`,
|
|
188
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_update
|
|
189
|
+
BEFORE UPDATE ON oh_authority_purges
|
|
190
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END`,
|
|
191
|
+
`CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_delete
|
|
192
|
+
BEFORE DELETE ON oh_authority_purges
|
|
193
|
+
BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END`,
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
export function normalizedSchemaSql(sql: string): string {
|
|
197
|
+
return sql.replace(/\bIF\s+NOT\s+EXISTS\b/giu, "").replace(/\s+/gu, " ").trim();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function expectedSchemaObject(statement: string): Readonly<{
|
|
201
|
+
name: string;
|
|
202
|
+
sql: string;
|
|
203
|
+
tableName: string;
|
|
204
|
+
type: "index" | "table" | "trigger";
|
|
205
|
+
}> {
|
|
206
|
+
const match = /^CREATE\s+(TABLE|INDEX|TRIGGER)(?:\s+IF\s+NOT\s+EXISTS)?\s+([a-z0-9_]+)/iu.exec(statement.trim());
|
|
207
|
+
if (match === null) throw new Error("Invalid compiled authority schema statement.");
|
|
208
|
+
const declaredType = match[1]?.toLowerCase();
|
|
209
|
+
const type = declaredType === "index" ? "index" as const
|
|
210
|
+
: declaredType === "trigger" ? "trigger" as const : "table" as const;
|
|
211
|
+
const name = match[2] as string;
|
|
212
|
+
const tableMatch = type === "index" || type === "trigger" ? /\bON\s+([a-z0-9_]+)/iu.exec(statement) : null;
|
|
213
|
+
const tableName = type === "table" ? name : tableMatch?.[1];
|
|
214
|
+
if (tableName === undefined) throw new Error("Invalid compiled authority index statement.");
|
|
215
|
+
return { name, sql: normalizedSchemaSql(statement), tableName, type };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const AUTHORITY_SCHEMA_OBJECTS = Object.freeze(
|
|
219
|
+
[AUTHORITY_SCHEMA_TABLE_STATEMENT, ...AUTHORITY_SCHEMA_STATEMENTS]
|
|
220
|
+
.map(expectedSchemaObject)
|
|
221
|
+
.sort((left, right) => canonicalJson([left.type, left.name])
|
|
222
|
+
.localeCompare(canonicalJson([right.type, right.name]))),
|
|
223
|
+
);
|
|
224
|
+
export const AUTHORITY_SCHEMA_SHA256 = canonicalSha256(AUTHORITY_SCHEMA_OBJECTS);
|
|
225
|
+
|
|
226
|
+
export function rowValue(
|
|
227
|
+
row: Readonly<Record<string, unknown>> | readonly unknown[],
|
|
228
|
+
key: string,
|
|
229
|
+
index: number,
|
|
230
|
+
): unknown {
|
|
231
|
+
return Array.isArray(row) ? row[index] : (row as Readonly<Record<string, unknown>>)[key];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function integer(value: unknown): number | null {
|
|
235
|
+
if (typeof value === "number") return Number.isSafeInteger(value) ? value : null;
|
|
236
|
+
if (typeof value === "bigint") {
|
|
237
|
+
const parsed = Number(value);
|
|
238
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function normalizeLimit(value: number | undefined, fallback = 100, maximum = 1000): number {
|
|
244
|
+
const limit = value ?? fallback;
|
|
245
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > maximum) {
|
|
246
|
+
throw new RangeError(`limit must be an integer from 1 through ${maximum}.`);
|
|
247
|
+
}
|
|
248
|
+
return limit;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function parseOperationJson(value: unknown): OhOperationV1 {
|
|
252
|
+
if (typeof value !== "string") throw new OhIntegrityError("A stored operation is not JSON text.");
|
|
253
|
+
let parsedValue: unknown;
|
|
254
|
+
try { parsedValue = JSON.parse(value); } catch { throw new OhIntegrityError("A stored operation is not JSON."); }
|
|
255
|
+
const operation = parseOhOperationV1(parsedValue);
|
|
256
|
+
if (operation === null || canonicalJson(operation) !== value) {
|
|
257
|
+
throw new OhIntegrityError("A stored operation is invalid.");
|
|
258
|
+
}
|
|
259
|
+
return operation;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function parseOperationRow(
|
|
263
|
+
row: Readonly<Record<string, unknown>> | readonly unknown[],
|
|
264
|
+
expected: Readonly<{ operationId?: string; operationSha256?: string; spaceId?: string }> = {},
|
|
265
|
+
): OhOperationV1 {
|
|
266
|
+
const operation = parseOperationJson(rowValue(row, "operation_json", 7));
|
|
267
|
+
if (rowValue(row, "operation_sha256", 0) !== operation.operationSha256
|
|
268
|
+
|| rowValue(row, "space_id", 1) !== operation.spaceId
|
|
269
|
+
|| integer(rowValue(row, "sequence", 2)) !== operation.sequence
|
|
270
|
+
|| rowValue(row, "operation_id", 3) !== operation.operationId
|
|
271
|
+
|| rowValue(row, "parent_operation_sha256", 4) !== operation.parentOperationSha256
|
|
272
|
+
|| rowValue(row, "graph_revision_sha256", 5) !== operation.graphRevisionSha256
|
|
273
|
+
|| rowValue(row, "records_sha256", 6) !== operation.recordsSha256
|
|
274
|
+
|| rowValue(row, "instant", 8) !== operation.instant
|
|
275
|
+
|| (expected.spaceId !== undefined && operation.spaceId !== expected.spaceId)
|
|
276
|
+
|| (expected.operationId !== undefined && operation.operationId !== expected.operationId)
|
|
277
|
+
|| (expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256)) {
|
|
278
|
+
throw new OhIntegrityError("Remote operation columns do not match their canonical envelope.");
|
|
279
|
+
}
|
|
280
|
+
return operation;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function parseBindingRow(
|
|
284
|
+
row: Readonly<Record<string, unknown>> | readonly unknown[],
|
|
285
|
+
expectedSpaceId: string,
|
|
286
|
+
): OhStoreBindingV1 {
|
|
287
|
+
const json = rowValue(row, "binding_json", 6);
|
|
288
|
+
if (typeof json !== "string") throw new OhIntegrityError("A remote store binding is not JSON text.");
|
|
289
|
+
let value: unknown;
|
|
290
|
+
try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A remote store binding is not JSON."); }
|
|
291
|
+
const binding = parseOhStoreBindingV1(value);
|
|
292
|
+
if (binding === null || canonicalJson(binding) !== json || binding.spaceId !== expectedSpaceId
|
|
293
|
+
|| rowValue(row, "space_id", 0) !== binding.spaceId
|
|
294
|
+
|| rowValue(row, "realm_id", 1) !== binding.realmId
|
|
295
|
+
|| rowValue(row, "profile_id", 2) !== binding.profile.profileId
|
|
296
|
+
|| rowValue(row, "profile_kind", 3) !== binding.profile.profileKind
|
|
297
|
+
|| rowValue(row, "profile_sha256", 4) !== binding.profile.profileSha256
|
|
298
|
+
|| rowValue(row, "binding_sha256", 5) !== binding.bindingSha256) {
|
|
299
|
+
throw new OhIntegrityError("Remote binding columns do not match their canonical envelope.");
|
|
300
|
+
}
|
|
301
|
+
return binding;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function parsePurgeReceiptRow(
|
|
305
|
+
row: Readonly<Record<string, unknown>> | readonly unknown[],
|
|
306
|
+
expectedSpaceId: string,
|
|
307
|
+
expectedBindingSha256?: Sha256Hex,
|
|
308
|
+
): OhSpacePurgeReceiptV1 {
|
|
309
|
+
const json = rowValue(row, "receipt_json", 6);
|
|
310
|
+
if (typeof json !== "string") throw new OhIntegrityError("A remote purge receipt is invalid.");
|
|
311
|
+
let value: unknown;
|
|
312
|
+
try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A remote purge receipt is invalid."); }
|
|
313
|
+
const receipt = parseOhSpacePurgeReceiptV1(value);
|
|
314
|
+
if (receipt === null || canonicalJson(receipt) !== json) {
|
|
315
|
+
throw new OhIntegrityError("A remote purge receipt is invalid.");
|
|
316
|
+
}
|
|
317
|
+
if (receipt.spaceId !== expectedSpaceId
|
|
318
|
+
|| (expectedBindingSha256 !== undefined && receipt.bindingSha256 !== expectedBindingSha256)
|
|
319
|
+
|| rowValue(row, "space_id", 0) !== receipt.spaceId
|
|
320
|
+
|| rowValue(row, "binding_sha256", 1) !== receipt.bindingSha256
|
|
321
|
+
|| rowValue(row, "prior_operation_sha256", 2) !== receipt.priorHead.operationSha256
|
|
322
|
+
|| integer(rowValue(row, "prior_sequence", 3)) !== receipt.priorHead.sequence
|
|
323
|
+
|| rowValue(row, "purged_at", 4) !== receipt.purgedAt
|
|
324
|
+
|| rowValue(row, "receipt_sha256", 5) !== receipt.receiptSha256) {
|
|
325
|
+
throw new OhIntegrityError("Remote purge columns do not match their canonical receipt.");
|
|
326
|
+
}
|
|
327
|
+
return receipt;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function parseHeadRow(row: Readonly<Record<string, unknown>> | readonly unknown[]): OhHeadV1 {
|
|
331
|
+
const generation = integer(rowValue(row, "generation", 0));
|
|
332
|
+
const graphValue = rowValue(row, "graph_revision_sha256", 1);
|
|
333
|
+
const operationValue = rowValue(row, "head_operation_sha256", 2);
|
|
334
|
+
const recordsSha256 = parseSha256Hex(rowValue(row, "records_sha256", 3));
|
|
335
|
+
const sequence = integer(rowValue(row, "sequence", 4));
|
|
336
|
+
const graphRevisionSha256 = graphValue === null ? null : parseSha256Hex(graphValue);
|
|
337
|
+
const operationSha256 = operationValue === null ? null : parseSha256Hex(operationValue);
|
|
338
|
+
if (generation === null || sequence === null || generation !== sequence || recordsSha256 === null
|
|
339
|
+
|| (graphValue !== null && graphRevisionSha256 === null)
|
|
340
|
+
|| (operationValue !== null && operationSha256 === null)
|
|
341
|
+
|| ((sequence === 0) !== (operationSha256 === null))
|
|
342
|
+
|| ((sequence === 0) !== (graphRevisionSha256 === null))) {
|
|
343
|
+
throw new OhIntegrityError("The remote authority contains an invalid head.");
|
|
344
|
+
}
|
|
345
|
+
return { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export const PURGE_PAYLOAD_TABLES = ["oh_authority_spaces", "oh_authority_bindings",
|
|
349
|
+
"oh_authority_operations", "oh_authority_operation_records", "oh_authority_records",
|
|
350
|
+
"oh_authority_dependencies"] as const;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { canonicalNow } from "./canonical";
|
|
3
|
+
import { OhConflictError, OhDependencyError, OhIntegrityError, OhOperationSizeError, OhProfileError, OhPurgedSpaceError } from "./store";
|
|
4
|
+
import type { OhLibSqlClientV1, OhLibSqlResultV1, OhLibSqlStatementV1 } from "./libsql-model";
|
|
5
|
+
|
|
6
|
+
export type LibSqlFailure = Readonly<{
|
|
7
|
+
_tag: "LibSqlConflict" | "LibSqlDependency" | "LibSqlIntegrity" | "LibSqlProfile" | "LibSqlPurged" | "LibSqlClosed"
|
|
8
|
+
| "LibSqlCapacity" | "LibSqlValidation" | "LibSqlForeign";
|
|
9
|
+
/** Kept locally to reproduce the existing public rejection value. */
|
|
10
|
+
cause: unknown;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
export function libSqlFailure(cause: unknown): LibSqlFailure {
|
|
14
|
+
return { _tag: cause instanceof OhConflictError ? "LibSqlConflict"
|
|
15
|
+
: cause instanceof OhDependencyError ? "LibSqlDependency"
|
|
16
|
+
: cause instanceof OhIntegrityError ? "LibSqlIntegrity"
|
|
17
|
+
: cause instanceof OhProfileError ? "LibSqlProfile"
|
|
18
|
+
: cause instanceof OhPurgedSpaceError ? "LibSqlPurged"
|
|
19
|
+
: cause instanceof OhOperationSizeError || cause instanceof RangeError ? "LibSqlCapacity"
|
|
20
|
+
: cause instanceof TypeError ? "LibSqlValidation" : "LibSqlForeign", cause };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Existing canonical validators may throw; unrelated generator defects remain defects. */
|
|
24
|
+
export function libSqlValue<A>(evaluate: () => A): Effect.Effect<A, LibSqlFailure> {
|
|
25
|
+
return Effect.try({ try: evaluate, catch: libSqlFailure });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LibSqlAuthorityClientService {
|
|
29
|
+
execute(statement: OhLibSqlStatementV1 | string): Effect.Effect<OhLibSqlResultV1, LibSqlFailure>;
|
|
30
|
+
batch(statements: readonly OhLibSqlStatementV1[], mode?: "deferred" | "read" | "write"):
|
|
31
|
+
Effect.Effect<readonly OhLibSqlResultV1[], LibSqlFailure>;
|
|
32
|
+
readonly close: Effect.Effect<void, LibSqlFailure>;
|
|
33
|
+
readonly currentInstant: Effect.Effect<string, LibSqlFailure>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class LibSqlAuthorityClient extends Context.Tag("@hraness/oh/LibSqlAuthorityClient")<
|
|
37
|
+
LibSqlAuthorityClient, LibSqlAuthorityClientService
|
|
38
|
+
>() {}
|
|
39
|
+
|
|
40
|
+
/** The structural client owns transaction atomicity. Never split a batch or
|
|
41
|
+
* abandon its foreign Promise when a fiber is interrupted; no retry is added. */
|
|
42
|
+
export function libSqlAuthorityClientLive(client: OhLibSqlClientV1): Layer.Layer<LibSqlAuthorityClient> {
|
|
43
|
+
const settle = <A>(call: () => Promise<A>): Effect.Effect<A, LibSqlFailure> =>
|
|
44
|
+
Effect.uninterruptible(Effect.tryPromise({ try: call, catch: libSqlFailure }));
|
|
45
|
+
return Layer.succeed(LibSqlAuthorityClient, {
|
|
46
|
+
execute: statement => settle(() => client.execute(statement)),
|
|
47
|
+
batch: (statements, mode) => settle(() => client.batch(statements, mode)),
|
|
48
|
+
close: libSqlValue(() => { client.close?.(); }),
|
|
49
|
+
currentInstant: libSqlValue(canonicalNow),
|
|
50
|
+
});
|
|
51
|
+
}
|