@hraness/oh 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +3 -0
- package/README.md +12 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15590 -1705
- package/dist/index.js +15209 -261
- package/dist/libsql-model.d.ts +75 -0
- package/dist/libsql-model.d.ts.map +1 -0
- package/dist/libsql-platform.d.ts +24 -0
- package/dist/libsql-platform.d.ts.map +1 -0
- package/dist/libsql-program.d.ts +64 -0
- package/dist/libsql-program.d.ts.map +1 -0
- package/dist/libsql-runtime.d.ts +13 -0
- package/dist/libsql-runtime.d.ts.map +1 -0
- package/dist/libsql.d.ts +4 -36
- package/dist/libsql.d.ts.map +1 -1
- package/dist/libsql.js +16583 -1426
- package/dist/memory-authority-platform.d.ts +210 -0
- package/dist/memory-authority-platform.d.ts.map +1 -0
- package/dist/memory-authority-program.d.ts +15 -0
- package/dist/memory-authority-program.d.ts.map +1 -0
- package/dist/memory-authority-runtime.d.ts +4 -0
- package/dist/memory-authority-runtime.d.ts.map +1 -0
- package/dist/memory-core.d.ts +522 -0
- package/dist/memory-core.d.ts.map +1 -0
- package/dist/memory.d.ts +5 -445
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +18232 -4143
- package/dist/sdk.js +15266 -318
- package/dist/semantic-cloud.js +1 -260
- package/dist/semantic-model.d.ts +75 -0
- package/dist/semantic-model.d.ts.map +1 -0
- package/dist/semantic-platform.d.ts +32 -0
- package/dist/semantic-platform.d.ts.map +1 -0
- package/dist/semantic-program.d.ts +18 -0
- package/dist/semantic-program.d.ts.map +1 -0
- package/dist/semantic-runtime.d.ts +6 -0
- package/dist/semantic-runtime.d.ts.map +1 -0
- package/dist/semantic.d.ts +6 -57
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +15541 -514
- package/dist/sqlite/index.js +1148 -1378
- package/dist/sqlite/port.d.ts +1 -1
- package/dist/sqlite/port.d.ts.map +1 -1
- package/dist/sync-libsql-program.d.ts +5 -0
- package/dist/sync-libsql-program.d.ts.map +1 -0
- package/dist/sync-model.d.ts +60 -0
- package/dist/sync-model.d.ts.map +1 -0
- package/dist/sync-platform.d.ts +41 -0
- package/dist/sync-platform.d.ts.map +1 -0
- package/dist/sync-program.d.ts +11 -0
- package/dist/sync-program.d.ts.map +1 -0
- package/dist/sync-runtime.d.ts +6 -0
- package/dist/sync-runtime.d.ts.map +1 -0
- package/dist/sync.d.ts +5 -55
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +15174 -226
- package/package.json +7 -5
- package/skills/oh/SKILL.md +3 -2
- package/spec/v1/store.md +14 -0
- package/src/cli.test.ts +35 -0
- package/src/cli.ts +4 -3
- package/src/cloudflare-embedding.ts +1 -1
- package/src/libsql-lifecycle.test.ts +76 -0
- package/src/libsql-model.ts +350 -0
- package/src/libsql-platform.ts +51 -0
- package/src/libsql-program.ts +1583 -0
- package/src/libsql-runtime.ts +89 -0
- package/src/libsql-semantic-v2.ts +1 -1
- package/src/libsql-semantic.ts +1 -1
- package/src/libsql.test.ts +245 -0
- package/src/libsql.ts +26 -1698
- package/src/memory-authority-platform.ts +133 -0
- package/src/memory-authority-program.ts +309 -0
- package/src/memory-authority-runtime.ts +40 -0
- package/src/memory-core.ts +2401 -0
- package/src/memory.test.ts +55 -0
- package/src/memory.ts +68 -2759
- package/src/semantic-model.ts +142 -0
- package/src/semantic-platform.ts +130 -0
- package/src/semantic-program.ts +113 -0
- package/src/semantic-runtime.ts +53 -0
- package/src/semantic.test.ts +115 -1
- package/src/semantic.ts +14 -324
- package/src/sqlite/port.ts +1 -1
- package/src/sync-libsql-program.ts +170 -0
- package/src/sync-lifecycle.test.ts +110 -0
- package/src/sync-model.ts +530 -0
- package/src/sync-platform.ts +87 -0
- package/src/sync-program.ts +117 -0
- package/src/sync-runtime.ts +33 -0
- package/src/sync.ts +10 -765
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { safeCode } from "./canonical";
|
|
3
|
+
import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
|
|
4
|
+
import { createOhSyncBundleV1, parseOhSyncBundleV1, parseOhSyncHeadV1,
|
|
5
|
+
type OhSyncHeadV1, type OhSyncResultV1 } from "./sync-model";
|
|
6
|
+
import { SyncStore, SyncTransport, type SyncFailure } from "./sync-platform";
|
|
7
|
+
|
|
8
|
+
export type SyncOptions = Readonly<{ batchSize?: number; maximumRounds?: number; remoteId?: string }>;
|
|
9
|
+
|
|
10
|
+
/** Only the store adapter can enter the synchronous SQLite transaction. */
|
|
11
|
+
export function synchronize(options: SyncOptions = {}):
|
|
12
|
+
Effect.Effect<OhSyncResultV1, SyncFailure, SyncStore | SyncTransport> {
|
|
13
|
+
return Effect.gen(function* () {
|
|
14
|
+
const store = yield* SyncStore;
|
|
15
|
+
const transport = yield* SyncTransport;
|
|
16
|
+
const batchSize = options.batchSize ?? 100;
|
|
17
|
+
const maximumRounds = options.maximumRounds ?? 100;
|
|
18
|
+
const remoteId = safeCode(options.remoteId ?? "default");
|
|
19
|
+
if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 1000
|
|
20
|
+
|| !Number.isSafeInteger(maximumRounds) || maximumRounds < 1 || maximumRounds > 10_000
|
|
21
|
+
|| remoteId === null) return yield* Effect.fail({ _tag: "ValidationFailure", cause: new TypeError("Invalid sync options.") } satisfies SyncFailure);
|
|
22
|
+
yield* transport.handshake(OH_CONTRACT_MANIFEST_V1);
|
|
23
|
+
let pulled = 0;
|
|
24
|
+
let pushed = 0;
|
|
25
|
+
const settled = (head: OhSyncHeadV1, rounds: number): Effect.Effect<OhSyncResultV1, SyncFailure> => Effect.gen(function* () {
|
|
26
|
+
yield* store.updateSyncState(remoteId, { pulledSequence: head.sequence,
|
|
27
|
+
pushedSequence: head.sequence, remoteHeadSha256: head.operationSha256 });
|
|
28
|
+
return { head, pulled, pushed, rounds, v: 1 };
|
|
29
|
+
});
|
|
30
|
+
for (let round = 1; round <= maximumRounds; round += 1) {
|
|
31
|
+
const remote = parseOhSyncHeadV1(yield* transport.head(store.spaceId));
|
|
32
|
+
if (remote === null) return yield* Effect.fail({ _tag: "ValidationFailure", cause: new Error("The sync transport returned an invalid head.") } satisfies SyncFailure);
|
|
33
|
+
const local = (yield* store.head);
|
|
34
|
+
if (local.sequence === remote.sequence) {
|
|
35
|
+
if (local.operationSha256 !== remote.operationSha256) {
|
|
36
|
+
return yield* Effect.fail({ _tag: "SyncConflict", cause: new Error("Sync conflict: equal sequence numbers have different heads.") } satisfies SyncFailure);
|
|
37
|
+
}
|
|
38
|
+
return yield* settled(remote, round);
|
|
39
|
+
}
|
|
40
|
+
if (local.sequence < remote.sequence) {
|
|
41
|
+
const bundle = parseOhSyncBundleV1(yield* transport.pull(store.spaceId, local.sequence, batchSize));
|
|
42
|
+
const terminal = bundle?.operations.at(-1);
|
|
43
|
+
if (bundle === null || bundle.operations.length === 0
|
|
44
|
+
|| bundle.operations.length > batchSize
|
|
45
|
+
|| bundle.spaceId !== store.spaceId
|
|
46
|
+
|| bundle.operations[0]?.sequence !== local.sequence + 1
|
|
47
|
+
|| bundle.operations[0]?.parentOperationSha256 !== local.operationSha256
|
|
48
|
+
|| terminal === undefined || terminal.sequence > remote.sequence
|
|
49
|
+
|| (terminal.sequence === remote.sequence
|
|
50
|
+
&& terminal.operationSha256 !== remote.operationSha256)) {
|
|
51
|
+
return yield* Effect.fail({ _tag: "SyncConflict", cause: new Error("Sync conflict: remote history does not extend the local head.") } satisfies SyncFailure);
|
|
52
|
+
}
|
|
53
|
+
yield* store.importOperations({
|
|
54
|
+
expectedHead: {
|
|
55
|
+
operationSha256: local.operationSha256,
|
|
56
|
+
sequence: local.sequence,
|
|
57
|
+
},
|
|
58
|
+
operations: bundle.operations,
|
|
59
|
+
});
|
|
60
|
+
pulled += bundle.operations.length;
|
|
61
|
+
const afterPull = (yield* store.head);
|
|
62
|
+
if (afterPull.sequence === remote.sequence
|
|
63
|
+
&& afterPull.operationSha256 === remote.operationSha256) {
|
|
64
|
+
const confirmed = parseOhSyncHeadV1(yield* transport.head(store.spaceId));
|
|
65
|
+
if (confirmed === null) return yield* Effect.fail({ _tag: "ValidationFailure", cause: new Error("The sync transport returned an invalid head.") } satisfies SyncFailure);
|
|
66
|
+
const confirmedLocal = (yield* store.head);
|
|
67
|
+
if (confirmed.sequence === remote.sequence
|
|
68
|
+
&& confirmed.operationSha256 === remote.operationSha256
|
|
69
|
+
&& confirmedLocal.sequence === confirmed.sequence
|
|
70
|
+
&& confirmedLocal.operationSha256 === confirmed.operationSha256) {
|
|
71
|
+
return yield* settled(confirmed, round);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
const candidates = (yield* store.changesSince({
|
|
76
|
+
operationSha256: remote.operationSha256,
|
|
77
|
+
sequence: remote.sequence,
|
|
78
|
+
}, {
|
|
79
|
+
limit: batchSize,
|
|
80
|
+
through: {
|
|
81
|
+
operationSha256: local.operationSha256,
|
|
82
|
+
sequence: local.sequence,
|
|
83
|
+
},
|
|
84
|
+
})).operations;
|
|
85
|
+
if (candidates.length === 0 || candidates[0]?.sequence !== remote.sequence + 1
|
|
86
|
+
|| candidates[0]?.parentOperationSha256 !== remote.operationSha256) {
|
|
87
|
+
return yield* Effect.fail({ _tag: "SyncConflict", cause: new Error("Sync conflict: local history does not extend the remote head.") } satisfies SyncFailure);
|
|
88
|
+
}
|
|
89
|
+
const bundle = createOhSyncBundleV1(store.spaceId, candidates, {
|
|
90
|
+
largestFittingPrefix: true,
|
|
91
|
+
});
|
|
92
|
+
const operations = bundle.operations;
|
|
93
|
+
const head = parseOhSyncHeadV1(yield* transport.push(bundle));
|
|
94
|
+
if (head === null) return yield* Effect.fail({ _tag: "ValidationFailure", cause: new Error("The sync transport returned an invalid push head.") } satisfies SyncFailure);
|
|
95
|
+
if (head.sequence !== operations.at(-1)?.sequence
|
|
96
|
+
|| head.operationSha256 !== operations.at(-1)?.operationSha256) {
|
|
97
|
+
return yield* Effect.fail({ _tag: "ValidationFailure", cause: new Error("The sync transport acknowledged a different head.") } satisfies SyncFailure);
|
|
98
|
+
}
|
|
99
|
+
pushed += operations.length;
|
|
100
|
+
const afterPush = (yield* store.head);
|
|
101
|
+
if (afterPush.sequence === head.sequence
|
|
102
|
+
&& afterPush.operationSha256 === head.operationSha256) {
|
|
103
|
+
const confirmed = parseOhSyncHeadV1(yield* transport.head(store.spaceId));
|
|
104
|
+
if (confirmed === null) return yield* Effect.fail({ _tag: "ValidationFailure", cause: new Error("The sync transport returned an invalid head.") } satisfies SyncFailure);
|
|
105
|
+
const confirmedLocal = (yield* store.head);
|
|
106
|
+
if (confirmed.sequence === head.sequence
|
|
107
|
+
&& confirmed.operationSha256 === head.operationSha256
|
|
108
|
+
&& confirmedLocal.sequence === confirmed.sequence
|
|
109
|
+
&& confirmedLocal.operationSha256 === confirmed.operationSha256) {
|
|
110
|
+
return yield* settled(confirmed, round);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return yield* Effect.fail({ _tag: "RoundLimit", cause: new Error("Sync did not settle within maximumRounds.") } satisfies SyncFailure);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, Layer, Option } from "effect";
|
|
2
|
+
import type { OhSqliteStore } from "./sqlite/store";
|
|
3
|
+
import type { LibSqlClientV1, OhOperationSyncTransportV1, OhSyncResultV1 } from "./sync-model";
|
|
4
|
+
import { makeLibSqlSyncTransport } from "./sync-libsql-program";
|
|
5
|
+
import { libSqlSyncClientLive, syncStoreLive, syncTransportLive, type SyncFailure } from "./sync-platform";
|
|
6
|
+
import { synchronize, type SyncOptions } from "./sync-program";
|
|
7
|
+
|
|
8
|
+
/** Closed runners retain the public facade's exact foreign rejection value. */
|
|
9
|
+
async function runClosed<A>(program: Effect.Effect<Exit.Exit<A, SyncFailure>, never, never>): Promise<A> {
|
|
10
|
+
const result = await Effect.runPromise(program);
|
|
11
|
+
if (Exit.isSuccess(result)) return result.value;
|
|
12
|
+
const expected = Cause.failureOption(result.cause);
|
|
13
|
+
if (Option.isSome(expected)) throw expected.value.cause;
|
|
14
|
+
throw Cause.squash(result.cause);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function runSyncBoundary(store: OhSqliteStore, transport: OhOperationSyncTransportV1,
|
|
18
|
+
options: SyncOptions): Promise<OhSyncResultV1> {
|
|
19
|
+
return runClosed(synchronize(options).pipe(
|
|
20
|
+
Effect.provide(Layer.merge(syncStoreLive(store), syncTransportLive(transport))),
|
|
21
|
+
Effect.exit,
|
|
22
|
+
));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function makeLibSqlSyncBoundary(client: LibSqlClientV1): OhOperationSyncTransportV1 {
|
|
26
|
+
const transport = Effect.runSync(makeLibSqlSyncTransport.pipe(Effect.provide(libSqlSyncClientLive(client))));
|
|
27
|
+
return {
|
|
28
|
+
handshake: (manifest) => runClosed(Effect.exit(transport.handshake(manifest))),
|
|
29
|
+
head: (spaceId) => runClosed(Effect.exit(transport.head(spaceId))),
|
|
30
|
+
pull: (spaceId, afterSequence, limit) => runClosed(Effect.exit(transport.pull(spaceId, afterSequence, limit))),
|
|
31
|
+
push: (bundle) => runClosed(Effect.exit(transport.push(bundle))),
|
|
32
|
+
};
|
|
33
|
+
}
|