@hraness/oh 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +3 -0
- package/README.md +4 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15590 -1705
- package/dist/index.js +15209 -261
- package/dist/libsql-model.d.ts +75 -0
- package/dist/libsql-model.d.ts.map +1 -0
- package/dist/libsql-platform.d.ts +24 -0
- package/dist/libsql-platform.d.ts.map +1 -0
- package/dist/libsql-program.d.ts +64 -0
- package/dist/libsql-program.d.ts.map +1 -0
- package/dist/libsql-runtime.d.ts +13 -0
- package/dist/libsql-runtime.d.ts.map +1 -0
- package/dist/libsql.d.ts +4 -36
- package/dist/libsql.d.ts.map +1 -1
- package/dist/libsql.js +16583 -1426
- package/dist/memory-authority-platform.d.ts +210 -0
- package/dist/memory-authority-platform.d.ts.map +1 -0
- package/dist/memory-authority-program.d.ts +15 -0
- package/dist/memory-authority-program.d.ts.map +1 -0
- package/dist/memory-authority-runtime.d.ts +4 -0
- package/dist/memory-authority-runtime.d.ts.map +1 -0
- package/dist/memory-core.d.ts +522 -0
- package/dist/memory-core.d.ts.map +1 -0
- package/dist/memory.d.ts +5 -445
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +18232 -4143
- package/dist/sdk.js +15266 -318
- package/dist/semantic-cloud.js +1 -260
- package/dist/semantic-model.d.ts +75 -0
- package/dist/semantic-model.d.ts.map +1 -0
- package/dist/semantic-platform.d.ts +32 -0
- package/dist/semantic-platform.d.ts.map +1 -0
- package/dist/semantic-program.d.ts +18 -0
- package/dist/semantic-program.d.ts.map +1 -0
- package/dist/semantic-runtime.d.ts +6 -0
- package/dist/semantic-runtime.d.ts.map +1 -0
- package/dist/semantic.d.ts +6 -57
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +15541 -514
- package/dist/sqlite/index.js +1148 -1378
- package/dist/sqlite/port.d.ts +1 -1
- package/dist/sqlite/port.d.ts.map +1 -1
- package/dist/sync-libsql-program.d.ts +5 -0
- package/dist/sync-libsql-program.d.ts.map +1 -0
- package/dist/sync-model.d.ts +60 -0
- package/dist/sync-model.d.ts.map +1 -0
- package/dist/sync-platform.d.ts +41 -0
- package/dist/sync-platform.d.ts.map +1 -0
- package/dist/sync-program.d.ts +11 -0
- package/dist/sync-program.d.ts.map +1 -0
- package/dist/sync-runtime.d.ts +6 -0
- package/dist/sync-runtime.d.ts.map +1 -0
- package/dist/sync.d.ts +5 -55
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +15174 -226
- package/package.json +7 -5
- package/skills/oh/SKILL.md +2 -1
- package/spec/v1/store.md +14 -0
- package/src/cli.test.ts +35 -0
- package/src/cli.ts +4 -3
- package/src/cloudflare-embedding.ts +1 -1
- package/src/libsql-lifecycle.test.ts +76 -0
- package/src/libsql-model.ts +350 -0
- package/src/libsql-platform.ts +51 -0
- package/src/libsql-program.ts +1583 -0
- package/src/libsql-runtime.ts +89 -0
- package/src/libsql-semantic-v2.ts +1 -1
- package/src/libsql-semantic.ts +1 -1
- package/src/libsql.test.ts +245 -0
- package/src/libsql.ts +26 -1698
- package/src/memory-authority-platform.ts +133 -0
- package/src/memory-authority-program.ts +309 -0
- package/src/memory-authority-runtime.ts +40 -0
- package/src/memory-core.ts +2401 -0
- package/src/memory.test.ts +55 -0
- package/src/memory.ts +68 -2759
- package/src/semantic-model.ts +142 -0
- package/src/semantic-platform.ts +130 -0
- package/src/semantic-program.ts +113 -0
- package/src/semantic-runtime.ts +53 -0
- package/src/semantic.test.ts +115 -1
- package/src/semantic.ts +14 -324
- package/src/sqlite/port.ts +1 -1
- package/src/sync-libsql-program.ts +170 -0
- package/src/sync-lifecycle.test.ts +110 -0
- package/src/sync-model.ts +530 -0
- package/src/sync-platform.ts +87 -0
- package/src/sync-program.ts +117 -0
- package/src/sync-runtime.ts +33 -0
- package/src/sync.ts +10 -765
package/src/semantic.ts
CHANGED
|
@@ -1,158 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { canonicalJson, canonicalSha256, hasExactKeys, isPlainRecord, parseSha256Hex,
|
|
5
|
-
safeCode, sha256Hex, type Sha256Hex } from "./canonical";
|
|
6
|
-
import { OH_GRAPH_LIMITS_V1, type KnowledgeGraphRecordV1 } from "./graph";
|
|
1
|
+
import type { KnowledgeGraphRecordV1 } from "./graph";
|
|
2
|
+
import { OH_EMBEDDING_PROFILE_V1, type OhSemanticSearchBackendV1, type QmdStoreFactoryV1 } from "./semantic-model";
|
|
3
|
+
import { makeSemanticBoundary } from "./semantic-runtime";
|
|
7
4
|
import type { OhSqliteStore } from "./sqlite/store";
|
|
8
5
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
documentFormat: "title: {title} | text: {content}",
|
|
14
|
-
engine: "@tobilu/qmd@2.5.3",
|
|
15
|
-
model: "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf",
|
|
16
|
-
normalization: "l2",
|
|
17
|
-
queryFormat: "task: search result | query: {query}",
|
|
18
|
-
v: 1,
|
|
19
|
-
} as const);
|
|
20
|
-
|
|
21
|
-
export type OhEmbeddingProfileV1 = typeof OH_EMBEDDING_PROFILE_V1;
|
|
22
|
-
|
|
23
|
-
export function formatOhEmbeddingQueryV1(query: string): string {
|
|
24
|
-
return `task: search result | query: ${query}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function formatOhEmbeddingDocumentV1(title: string, content: string): string {
|
|
28
|
-
return `title: ${title} | text: ${content}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function normalizeOhEmbeddingV1(vector: readonly number[]): readonly number[] {
|
|
32
|
-
if (vector.length !== OH_EMBEDDING_PROFILE_V1.dimensions
|
|
33
|
-
|| vector.some((component) => !Number.isFinite(component))) {
|
|
34
|
-
throw new TypeError(`Embedding vectors must contain ${OH_EMBEDDING_PROFILE_V1.dimensions} finite values.`);
|
|
35
|
-
}
|
|
36
|
-
const scale = vector.reduce((maximum, component) => Math.max(maximum, Math.abs(component)), 0);
|
|
37
|
-
if (scale === 0) throw new TypeError("Embedding vectors must have nonzero magnitude.");
|
|
38
|
-
const scaledMagnitude = Math.sqrt(vector.reduce((sum, component) => {
|
|
39
|
-
const scaled = component / scale;
|
|
40
|
-
return sum + scaled * scaled;
|
|
41
|
-
}, 0));
|
|
42
|
-
if (!Number.isFinite(scaledMagnitude) || scaledMagnitude === 0) {
|
|
43
|
-
throw new TypeError("Embedding vectors must have finite nonzero magnitude.");
|
|
44
|
-
}
|
|
45
|
-
const normalized = vector.map((component) => (component / scale) / scaledMagnitude);
|
|
46
|
-
if (normalized.some((component) => !Number.isFinite(component))) {
|
|
47
|
-
throw new TypeError("Embedding vectors must normalize to finite values.");
|
|
48
|
-
}
|
|
49
|
-
return normalized;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function cosineSimilarityV1(left: readonly number[], right: readonly number[]): number {
|
|
53
|
-
const normalizedLeft = normalizeOhEmbeddingV1(left);
|
|
54
|
-
const normalizedRight = normalizeOhEmbeddingV1(right);
|
|
55
|
-
return normalizedLeft.reduce((sum, component, index) => sum + component * (normalizedRight[index] as number), 0);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type OhSemanticSearchResultV1 = Readonly<{
|
|
59
|
-
key: string;
|
|
60
|
-
recordSha256: Sha256Hex;
|
|
61
|
-
score: number;
|
|
62
|
-
v: 1;
|
|
63
|
-
}>;
|
|
64
|
-
|
|
65
|
-
export interface OhSemanticSearchBackendV1 {
|
|
66
|
-
readonly profile: OhEmbeddingProfileV1;
|
|
67
|
-
close(): Promise<void>;
|
|
68
|
-
index(records: readonly KnowledgeGraphRecordV1[]): Promise<Readonly<{ indexed: number; v: 1 }>>;
|
|
69
|
-
search(query: string, limit: number, authority: OhSqliteStore): Promise<readonly OhSemanticSearchResultV1[]>;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
type QmdStore = {
|
|
73
|
-
close(): Promise<void>;
|
|
74
|
-
embed(options: Readonly<{ collection: string; model: string }>): Promise<unknown>;
|
|
75
|
-
searchVector(query: string, options: Readonly<{ collection: string; limit: number }>): Promise<unknown>;
|
|
76
|
-
update(options: Readonly<{ collections: readonly string[] }>): Promise<unknown>;
|
|
77
|
-
};
|
|
78
|
-
export type QmdStoreFactoryV1 = (options: Readonly<{
|
|
79
|
-
config: Readonly<Record<string, unknown>>;
|
|
80
|
-
dbPath: string;
|
|
81
|
-
}>) => Promise<QmdStore>;
|
|
82
|
-
|
|
83
|
-
const qmdModuleSpecifier: string = "@tobilu/qmd";
|
|
84
|
-
async function defaultQmdStoreFactory(options: Parameters<QmdStoreFactoryV1>[0]): Promise<QmdStore> {
|
|
85
|
-
let module: unknown;
|
|
86
|
-
try { module = await import(qmdModuleSpecifier); } catch {
|
|
87
|
-
throw new Error("Semantic search needs the optional @tobilu/qmd@2.5.3 package.");
|
|
88
|
-
}
|
|
89
|
-
const createStore = (module as { createStore?: unknown }).createStore;
|
|
90
|
-
if (typeof createStore !== "function") throw new Error("The installed QMD package has no compatible createStore export.");
|
|
91
|
-
return await (createStore as QmdStoreFactoryV1)(options);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
type SemanticManifestV1 = Readonly<{
|
|
95
|
-
entries: Readonly<Record<string, Readonly<{ key: string; recordSha256: Sha256Hex }>>>;
|
|
96
|
-
profileSha256: Sha256Hex;
|
|
97
|
-
v: 1;
|
|
98
|
-
}>;
|
|
99
|
-
|
|
100
|
-
function recordDocument(record: KnowledgeGraphRecordV1): string {
|
|
101
|
-
return `# ${record.key}\n\nkind: ${record.kind}\n\n${canonicalJson(record.value)}\n`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const QMD_VECTOR_RESULT_KEYS = [
|
|
105
|
-
"body", "bodyLength", "chunkPos", "collectionName", "context", "displayPath", "docid",
|
|
106
|
-
"filepath", "hash", "modifiedAt", "score", "source", "title",
|
|
107
|
-
] as const;
|
|
108
|
-
type ParsedQmdVectorResult = Readonly<{
|
|
109
|
-
body: string;
|
|
110
|
-
filename: string;
|
|
111
|
-
hash: Sha256Hex;
|
|
112
|
-
score: number;
|
|
113
|
-
title: string;
|
|
114
|
-
}>;
|
|
115
|
-
|
|
116
|
-
function semanticManifest(entries: Record<string, { key: string; recordSha256: Sha256Hex }>): SemanticManifestV1 {
|
|
117
|
-
const immutableEntries: Record<string, Readonly<{ key: string; recordSha256: Sha256Hex }>> = {};
|
|
118
|
-
for (const [filename, entry] of Object.entries(entries)) {
|
|
119
|
-
immutableEntries[filename] = Object.freeze({ ...entry });
|
|
120
|
-
}
|
|
121
|
-
return Object.freeze({
|
|
122
|
-
entries: Object.freeze(immutableEntries),
|
|
123
|
-
profileSha256: canonicalSha256(OH_EMBEDDING_PROFILE_V1),
|
|
124
|
-
v: 1,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function parseQmdVectorResult(value: unknown): ParsedQmdVectorResult | null {
|
|
129
|
-
if (!isPlainRecord(value) || !hasExactKeys(value, QMD_VECTOR_RESULT_KEYS)) return null;
|
|
130
|
-
const pathMatch = typeof value.filepath === "string"
|
|
131
|
-
? /^qmd:\/\/oh\/([a-f0-9]{64}\.md)$/u.exec(value.filepath)
|
|
132
|
-
: null;
|
|
133
|
-
const filename = pathMatch?.[1];
|
|
134
|
-
const hash = parseSha256Hex(value.hash);
|
|
135
|
-
const title = safeCode(value.title, 512);
|
|
136
|
-
if (filename === undefined || value.displayPath !== `oh/${filename}` || value.collectionName !== "oh"
|
|
137
|
-
|| value.source !== "vec" || value.context !== null || value.modifiedAt !== ""
|
|
138
|
-
|| hash === null || value.docid !== hash.slice(0, 6) || title === null
|
|
139
|
-
|| typeof value.body !== "string" || typeof value.bodyLength !== "number" || !Number.isSafeInteger(value.bodyLength)
|
|
140
|
-
|| value.bodyLength < 0 || value.bodyLength > OH_GRAPH_LIMITS_V1.recordBytes + 4096
|
|
141
|
-
|| value.body.length !== value.bodyLength || hash !== sha256Hex(value.body) || typeof value.chunkPos !== "number"
|
|
142
|
-
|| !Number.isSafeInteger(value.chunkPos) || value.chunkPos < 0
|
|
143
|
-
|| typeof value.score !== "number" || !Number.isFinite(value.score) || value.score < 0 || value.score > 1) {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
return { body: value.body, filename, hash, score: value.score, title };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function parseQmdVectorResults(value: unknown): readonly ParsedQmdVectorResult[] {
|
|
150
|
-
if (!Array.isArray(value) || value.length > 100) {
|
|
151
|
-
throw new Error("QMD returned an invalid vector result batch.");
|
|
152
|
-
}
|
|
153
|
-
return value.map(parseQmdVectorResult)
|
|
154
|
-
.filter((result): result is ParsedQmdVectorResult => result !== null);
|
|
155
|
-
}
|
|
6
|
+
export { OH_EMBEDDING_PROFILE_V1, cosineSimilarityV1, formatOhEmbeddingDocumentV1,
|
|
7
|
+
formatOhEmbeddingQueryV1, normalizeOhEmbeddingV1 } from "./semantic-model";
|
|
8
|
+
export type { OhEmbeddingProfileV1, OhSemanticSearchBackendV1,
|
|
9
|
+
OhSemanticSearchResultV1, QmdStoreFactoryV1 } from "./semantic-model";
|
|
156
10
|
|
|
157
11
|
/**
|
|
158
12
|
* Optional, rebuildable local QMD index. SQLite records remain authoritative;
|
|
@@ -160,185 +14,21 @@ function parseQmdVectorResults(value: unknown): readonly ParsedQmdVectorResult[]
|
|
|
160
14
|
*/
|
|
161
15
|
export class OhQmdSemanticBackendV1 implements OhSemanticSearchBackendV1 {
|
|
162
16
|
readonly profile = OH_EMBEDDING_PROFILE_V1;
|
|
163
|
-
readonly #
|
|
164
|
-
readonly #databasePath: string;
|
|
165
|
-
readonly #factory: QmdStoreFactoryV1;
|
|
166
|
-
#manifest: SemanticManifestV1 = semanticManifest({});
|
|
167
|
-
#manifestLoad: Promise<void> | null = null;
|
|
168
|
-
#store: Promise<QmdStore> | null = null;
|
|
169
|
-
#storeClose: Promise<void> | null = null;
|
|
170
|
-
#closure: Promise<void> | null = null;
|
|
171
|
-
#indexQueue: Promise<void> = Promise.resolve();
|
|
172
|
-
readonly #activeSearches = new Set<Promise<unknown>>();
|
|
173
|
-
#closed = false;
|
|
17
|
+
readonly #boundary;
|
|
174
18
|
|
|
175
19
|
constructor(options: Readonly<{ cacheDirectory: string; databasePath?: string; storeFactory?: QmdStoreFactoryV1 }>) {
|
|
176
|
-
this.#
|
|
177
|
-
this.#databasePath = resolve(options.databasePath ?? join(this.#cacheDirectory, "qmd.sqlite"));
|
|
178
|
-
this.#factory = options.storeFactory ?? defaultQmdStoreFactory;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async #open(): Promise<QmdStore> {
|
|
182
|
-
if (this.#closed) throw new Error("The semantic backend is closed.");
|
|
183
|
-
this.#store ??= this.#initializeStore();
|
|
184
|
-
const store = await this.#store;
|
|
185
|
-
if (this.#closed) {
|
|
186
|
-
await this.#closeStore(store);
|
|
187
|
-
throw new Error("The semantic backend is closed.");
|
|
188
|
-
}
|
|
189
|
-
return store;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async #initializeStore(): Promise<QmdStore> {
|
|
193
|
-
const documents = join(this.#cacheDirectory, "documents");
|
|
194
|
-
await mkdir(documents, { recursive: true });
|
|
195
|
-
await this.#loadManifest();
|
|
196
|
-
if (this.#closed) throw new Error("The semantic backend is closed.");
|
|
197
|
-
const store = await this.#factory({
|
|
198
|
-
dbPath: this.#databasePath,
|
|
199
|
-
config: {
|
|
200
|
-
collections: { oh: { path: documents, pattern: "*.md" } },
|
|
201
|
-
models: { embed: OH_EMBEDDING_PROFILE_V1.model },
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
if (this.#closed) {
|
|
205
|
-
await this.#closeStore(store);
|
|
206
|
-
throw new Error("The semantic backend is closed.");
|
|
207
|
-
}
|
|
208
|
-
return store;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
#closeStore(store: QmdStore): Promise<void> {
|
|
212
|
-
this.#storeClose ??= Promise.resolve().then(() => store.close());
|
|
213
|
-
return this.#storeClose;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
#loadManifest(): Promise<void> {
|
|
217
|
-
this.#manifestLoad ??= this.#readManifest();
|
|
218
|
-
return this.#manifestLoad;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async #readManifest(): Promise<void> {
|
|
222
|
-
let text: string;
|
|
223
|
-
try { text = await readFile(join(this.#cacheDirectory, "manifest.json"), "utf8"); }
|
|
224
|
-
catch (error) {
|
|
225
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
let value: unknown;
|
|
229
|
-
try { value = JSON.parse(text); } catch { throw new Error("The semantic manifest is not JSON."); }
|
|
230
|
-
if (canonicalJson(value) !== text || !isPlainRecord(value)
|
|
231
|
-
|| !hasExactKeys(value, ["entries", "profileSha256", "v"]) || value.v !== 1
|
|
232
|
-
|| value.profileSha256 !== canonicalSha256(OH_EMBEDDING_PROFILE_V1) || !isPlainRecord(value.entries)
|
|
233
|
-
|| Object.keys(value.entries).length > 65_536) throw new Error("The semantic manifest is incompatible or invalid.");
|
|
234
|
-
const entries: Record<string, { key: string; recordSha256: Sha256Hex }> = {};
|
|
235
|
-
for (const [filename, candidate] of Object.entries(value.entries)) {
|
|
236
|
-
if (!/^[a-f0-9]{64}\.md$/u.test(filename) || !isPlainRecord(candidate)
|
|
237
|
-
|| !hasExactKeys(candidate, ["key", "recordSha256"])) throw new Error("The semantic manifest has an invalid entry.");
|
|
238
|
-
const key = safeCode(candidate.key, 512);
|
|
239
|
-
const recordSha256 = parseSha256Hex(candidate.recordSha256);
|
|
240
|
-
if (key === null || recordSha256 === null || filename !== `${sha256Hex(key)}.md`) {
|
|
241
|
-
throw new Error("The semantic manifest entry identity is invalid.");
|
|
242
|
-
}
|
|
243
|
-
entries[filename] = { key, recordSha256 };
|
|
244
|
-
}
|
|
245
|
-
this.#manifest = semanticManifest(entries);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
index(records: readonly KnowledgeGraphRecordV1[]): Promise<Readonly<{ indexed: number; v: 1 }>> {
|
|
249
|
-
if (records.length > 65_536) {
|
|
250
|
-
return Promise.reject(new RangeError("A semantic snapshot may contain at most 65,536 records."));
|
|
251
|
-
}
|
|
252
|
-
if (this.#closed) return Promise.reject(new Error("The semantic backend is closed."));
|
|
253
|
-
const snapshot = [...records];
|
|
254
|
-
const operation = this.#indexQueue.then(() => this.#indexSnapshot(snapshot));
|
|
255
|
-
this.#indexQueue = operation.then(() => undefined, () => undefined);
|
|
256
|
-
return operation;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async #indexSnapshot(records: readonly KnowledgeGraphRecordV1[]): Promise<Readonly<{ indexed: number; v: 1 }>> {
|
|
260
|
-
if (this.#closed) throw new Error("The semantic backend is closed.");
|
|
261
|
-
const documents = join(this.#cacheDirectory, "documents");
|
|
262
|
-
await mkdir(documents, { recursive: true });
|
|
263
|
-
await this.#loadManifest();
|
|
264
|
-
const entries: Record<string, { key: string; recordSha256: Sha256Hex }> = {};
|
|
265
|
-
for (const record of records) {
|
|
266
|
-
const filename = `${sha256Hex(record.key)}.md`;
|
|
267
|
-
entries[filename] = { key: record.key, recordSha256: record.recordSha256 };
|
|
268
|
-
const path = join(documents, filename);
|
|
269
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
270
|
-
await writeFile(temporary, recordDocument(record), { encoding: "utf8", mode: 0o600 });
|
|
271
|
-
await rename(temporary, path);
|
|
272
|
-
}
|
|
273
|
-
const retained = new Set(Object.keys(entries));
|
|
274
|
-
for (const filename of await readdir(documents)) {
|
|
275
|
-
if (/^[a-f0-9]{64}\.md$/u.test(filename) && !retained.has(filename)) await unlink(join(documents, filename));
|
|
276
|
-
}
|
|
277
|
-
const nextManifest = semanticManifest(entries);
|
|
278
|
-
const store = await this.#open();
|
|
279
|
-
await store.update({ collections: ["oh"] });
|
|
280
|
-
await store.embed({ collection: "oh", model: OH_EMBEDDING_PROFILE_V1.model });
|
|
281
|
-
const manifestPath = join(this.#cacheDirectory, "manifest.json");
|
|
282
|
-
const temporaryManifest = `${manifestPath}.${process.pid}.tmp`;
|
|
283
|
-
await writeFile(temporaryManifest, canonicalJson(nextManifest), { encoding: "utf8", mode: 0o600 });
|
|
284
|
-
await rename(temporaryManifest, manifestPath);
|
|
285
|
-
this.#manifest = nextManifest;
|
|
286
|
-
return { indexed: records.length, v: 1 };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
search(query: string, limit: number, authority: OhSqliteStore): Promise<readonly OhSemanticSearchResultV1[]> {
|
|
290
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
291
|
-
return Promise.reject(new RangeError("Semantic limit must be 1 through 100."));
|
|
292
|
-
}
|
|
293
|
-
if (this.#closed) return Promise.reject(new Error("The semantic backend is closed."));
|
|
294
|
-
const operation = this.#searchSnapshot(query, limit, authority);
|
|
295
|
-
this.#activeSearches.add(operation);
|
|
296
|
-
void operation.then(
|
|
297
|
-
() => { this.#activeSearches.delete(operation); },
|
|
298
|
-
() => { this.#activeSearches.delete(operation); },
|
|
299
|
-
);
|
|
300
|
-
return operation;
|
|
20
|
+
this.#boundary = makeSemanticBoundary(options);
|
|
301
21
|
}
|
|
302
22
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const store = await this.#open();
|
|
306
|
-
const manifest = this.#manifest;
|
|
307
|
-
const results = parseQmdVectorResults(await store.searchVector(query,
|
|
308
|
-
{ collection: "oh", limit: Math.min(100, limit * 3) }));
|
|
309
|
-
const output: OhSemanticSearchResultV1[] = [];
|
|
310
|
-
const seen = new Set<string>();
|
|
311
|
-
for (const result of results) {
|
|
312
|
-
const entry = manifest.entries[result.filename];
|
|
313
|
-
if (entry === undefined || result.title !== entry.key || seen.has(entry.key)) continue;
|
|
314
|
-
const current = authority.get(entry.key);
|
|
315
|
-
if (current === null || current.recordSha256 !== entry.recordSha256) continue;
|
|
316
|
-
const expectedDocument = recordDocument(current);
|
|
317
|
-
if (result.body !== expectedDocument || result.hash !== sha256Hex(expectedDocument)) continue;
|
|
318
|
-
seen.add(entry.key);
|
|
319
|
-
output.push({ key: entry.key, recordSha256: entry.recordSha256, score: result.score, v: 1 });
|
|
320
|
-
if (output.length === limit) break;
|
|
321
|
-
}
|
|
322
|
-
return output;
|
|
23
|
+
index(records: readonly KnowledgeGraphRecordV1[]): ReturnType<OhSemanticSearchBackendV1["index"]> {
|
|
24
|
+
return this.#boundary.index(records);
|
|
323
25
|
}
|
|
324
26
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
await Promise.allSettled([...this.#activeSearches]);
|
|
328
|
-
if (this.#store === null) return;
|
|
329
|
-
try {
|
|
330
|
-
const store = await this.#store;
|
|
331
|
-
await this.#closeStore(store);
|
|
332
|
-
} catch {
|
|
333
|
-
// Initialization failures are not close failures. A close initiated by
|
|
334
|
-
// late initialization is shared separately and must still be observed.
|
|
335
|
-
if (this.#storeClose !== null) await this.#storeClose;
|
|
336
|
-
}
|
|
27
|
+
search(query: string, limit: number, authority: OhSqliteStore): ReturnType<OhSemanticSearchBackendV1["search"]> {
|
|
28
|
+
return this.#boundary.search(query, limit, authority);
|
|
337
29
|
}
|
|
338
30
|
|
|
339
31
|
close(): Promise<void> {
|
|
340
|
-
this.#
|
|
341
|
-
this.#closure ??= this.#finishClose();
|
|
342
|
-
return this.#closure;
|
|
32
|
+
return this.#boundary.close();
|
|
343
33
|
}
|
|
344
34
|
}
|
package/src/sqlite/port.ts
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Deferred, Effect, Exit, Option, Ref } from "effect";
|
|
2
|
+
import { canonicalJson, parseSha256Hex, safeCode, utf8ByteLength } from "./canonical";
|
|
3
|
+
import { OH_CONTRACT_MANIFEST_V1, parseOhContractManifestV1, type OhContractManifestV1 } from "./contract";
|
|
4
|
+
import { OH_OPERATION_MAX_BYTES_V1, parseOhOperationV1, type OhOperationV1 } from "./operation";
|
|
5
|
+
import { createOhSyncBundleV1, parseOhSyncBundleV1, OH_SYNC_BUNDLE_MAX_OPERATIONS_V1,
|
|
6
|
+
syncIngressBundleBudgetV1, type OhSyncHeadV1 } from "./sync-model";
|
|
7
|
+
import { LibSqlSyncClient, syncInvalid, type SyncFailure, type SyncTransportService } from "./sync-platform";
|
|
8
|
+
|
|
9
|
+
function invalid(message: string): SyncFailure {
|
|
10
|
+
return { _tag: "ValidationFailure", cause: new Error(message) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function conflict(message: string): SyncFailure {
|
|
14
|
+
return { _tag: "SyncConflict", cause: new Error(message) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function rowValue(row: Readonly<Record<string, unknown>> | readonly unknown[], key: string, index: number): unknown {
|
|
18
|
+
return Array.isArray(row) ? row[index] : (row as Readonly<Record<string, unknown>>)[key];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A shared initialization attempt retains its result for every admitted waiter. */
|
|
22
|
+
export const makeLibSqlSyncTransport: Effect.Effect<SyncTransportService, never, LibSqlSyncClient> = Effect.gen(function* () {
|
|
23
|
+
const client = yield* LibSqlSyncClient;
|
|
24
|
+
const ready = yield* Ref.make<Option.Option<Deferred.Deferred<void, SyncFailure>>>(Option.none());
|
|
25
|
+
const setup = (manifest: OhContractManifestV1): Effect.Effect<void, SyncFailure> => Effect.gen(function* () {
|
|
26
|
+
if (parseOhContractManifestV1(manifest) === null) return yield* Effect.fail(invalid("Unsupported contract manifest."));
|
|
27
|
+
yield* client.batch([
|
|
28
|
+
{ sql: `CREATE TABLE IF NOT EXISTS oh_sync_contracts (
|
|
29
|
+
contract_id TEXT PRIMARY KEY, contract_sha256 TEXT NOT NULL, manifest_json TEXT NOT NULL
|
|
30
|
+
) STRICT` },
|
|
31
|
+
{ sql: `CREATE TABLE IF NOT EXISTS oh_sync_operations (
|
|
32
|
+
space_id TEXT NOT NULL, sequence INTEGER NOT NULL, operation_sha256 TEXT NOT NULL UNIQUE,
|
|
33
|
+
operation_json TEXT NOT NULL, PRIMARY KEY(space_id, sequence)
|
|
34
|
+
) STRICT` },
|
|
35
|
+
{ sql: "INSERT INTO oh_sync_contracts(contract_id, contract_sha256, manifest_json) VALUES (?, ?, ?) ON CONFLICT(contract_id) DO NOTHING",
|
|
36
|
+
args: [manifest.contractId, manifest.contractSha256, canonicalJson(manifest)] },
|
|
37
|
+
], "write");
|
|
38
|
+
const result = yield* client.execute({ sql: "SELECT contract_sha256, manifest_json FROM oh_sync_contracts WHERE contract_id = ?",
|
|
39
|
+
args: [manifest.contractId] });
|
|
40
|
+
const row = result.rows[0];
|
|
41
|
+
if (row === undefined || rowValue(row, "contract_sha256", 0) !== manifest.contractSha256
|
|
42
|
+
|| rowValue(row, "manifest_json", 1) !== canonicalJson(manifest)) {
|
|
43
|
+
return yield* Effect.fail(invalid("Remote contract manifest mismatch."));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const ensure = (manifest = OH_CONTRACT_MANIFEST_V1): Effect.Effect<void, SyncFailure> => Effect.uninterruptible(
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const candidate = yield* Deferred.make<void, SyncFailure>();
|
|
50
|
+
const selected = yield* Ref.modify(ready, (current) => Option.isSome(current)
|
|
51
|
+
? [current.value, current] as const
|
|
52
|
+
: [candidate, Option.some(candidate)] as const);
|
|
53
|
+
if (selected !== candidate) return yield* Deferred.await(selected);
|
|
54
|
+
const result = yield* Effect.exit(setup(manifest));
|
|
55
|
+
// A failed shared attempt may be retried by a later call, while its
|
|
56
|
+
// admitted waiters all observe this exact failure, including defects.
|
|
57
|
+
if (Exit.isFailure(result)) yield* Ref.set(ready, Option.none());
|
|
58
|
+
yield* Deferred.done(candidate, result);
|
|
59
|
+
return yield* result;
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const head = (spaceId: string): Effect.Effect<OhSyncHeadV1, SyncFailure> => Effect.gen(function* () {
|
|
64
|
+
yield* ensure();
|
|
65
|
+
const result = yield* client.execute({ sql: `SELECT sequence, operation_sha256 FROM oh_sync_operations
|
|
66
|
+
WHERE space_id = ? ORDER BY sequence DESC LIMIT 1`, args: [spaceId] });
|
|
67
|
+
const row = result.rows[0];
|
|
68
|
+
if (row === undefined) return { operationSha256: null, sequence: 0, v: 1 };
|
|
69
|
+
const sequence = Number(rowValue(row, "sequence", 0));
|
|
70
|
+
const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
|
|
71
|
+
if (!Number.isSafeInteger(sequence) || sequence < 1 || operationSha256 === null) {
|
|
72
|
+
return yield* Effect.fail(invalid("Invalid remote head."));
|
|
73
|
+
}
|
|
74
|
+
return { operationSha256, sequence, v: 1 };
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
handshake: (manifest) => Effect.gen(function* () {
|
|
78
|
+
const parsed = parseOhContractManifestV1(manifest);
|
|
79
|
+
if (parsed === null) return yield* Effect.fail(invalid("Unsupported contract manifest."));
|
|
80
|
+
yield* ensure(parsed);
|
|
81
|
+
}),
|
|
82
|
+
head,
|
|
83
|
+
pull: (spaceId, afterSequence, limit) => Effect.gen(function* () {
|
|
84
|
+
const parsedSpaceId = safeCode(spaceId);
|
|
85
|
+
if (parsedSpaceId === null
|
|
86
|
+
|| !Number.isSafeInteger(afterSequence) || afterSequence < 0 || Object.is(afterSequence, -0)
|
|
87
|
+
|| !Number.isSafeInteger(limit) || limit < 1 || limit > OH_SYNC_BUNDLE_MAX_OPERATIONS_V1) {
|
|
88
|
+
return yield* Effect.fail(syncInvalid(new TypeError("Invalid sync pull request.")));
|
|
89
|
+
}
|
|
90
|
+
yield* ensure();
|
|
91
|
+
const bundleBudget = syncIngressBundleBudgetV1(parsedSpaceId);
|
|
92
|
+
const result = yield* client.execute({ sql: `SELECT operation_json FROM (
|
|
93
|
+
SELECT sequence, operation_json,
|
|
94
|
+
row_number() OVER (ORDER BY sequence) AS ordinal,
|
|
95
|
+
sum(length(CAST(operation_json AS BLOB))) OVER (
|
|
96
|
+
ORDER BY sequence ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
|
97
|
+
) AS cumulative_bytes
|
|
98
|
+
FROM (
|
|
99
|
+
SELECT sequence, operation_json FROM oh_sync_operations
|
|
100
|
+
WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?
|
|
101
|
+
)
|
|
102
|
+
) WHERE ordinal = 1 OR cumulative_bytes + ordinal - 1 <= ? ORDER BY sequence`,
|
|
103
|
+
args: [parsedSpaceId, afterSequence, limit,
|
|
104
|
+
bundleBudget.maximumBytes - bundleBudget.bytes] });
|
|
105
|
+
const rows = result.rows;
|
|
106
|
+
if (!Array.isArray(rows) || rows.length > limit) {
|
|
107
|
+
return yield* Effect.fail(invalid("Remote sync pull exceeded its requested row limit."));
|
|
108
|
+
}
|
|
109
|
+
const operations: OhOperationV1[] = [];
|
|
110
|
+
for (const row of rows) {
|
|
111
|
+
const json = rowValue(row, "operation_json", 0);
|
|
112
|
+
if (typeof json !== "string") return yield* Effect.fail(invalid("Invalid remote operation JSON."));
|
|
113
|
+
if (utf8ByteLength(json) > OH_OPERATION_MAX_BYTES_V1) {
|
|
114
|
+
return yield* Effect.fail(invalid("Remote operation JSON exceeds the canonical operation byte limit."));
|
|
115
|
+
}
|
|
116
|
+
const raw: unknown = yield* Effect.try({ try: () => JSON.parse(json) as unknown, catch: syncInvalid });
|
|
117
|
+
const operation = parseOhOperationV1(raw);
|
|
118
|
+
if (operation === null || canonicalJson(operation) !== json) return yield* Effect.fail(invalid("Invalid remote operation."));
|
|
119
|
+
operations.push(operation);
|
|
120
|
+
}
|
|
121
|
+
return yield* Effect.try({ try: () => createOhSyncBundleV1(parsedSpaceId, operations), catch: syncInvalid });
|
|
122
|
+
}),
|
|
123
|
+
push: (value) => Effect.gen(function* () {
|
|
124
|
+
yield* ensure();
|
|
125
|
+
const bundle = parseOhSyncBundleV1(value);
|
|
126
|
+
if (bundle === null) return yield* Effect.fail(invalid("Invalid outgoing sync bundle."));
|
|
127
|
+
if (bundle.operations.length === 0) return yield* head(bundle.spaceId);
|
|
128
|
+
const remote = yield* head(bundle.spaceId);
|
|
129
|
+
const first = bundle.operations[0];
|
|
130
|
+
const last = bundle.operations.at(-1);
|
|
131
|
+
if (first === undefined || last === undefined) return yield* Effect.fail(invalid("Invalid outgoing sync bundle."));
|
|
132
|
+
if (remote.sequence >= last.sequence) {
|
|
133
|
+
const result = yield* client.execute({ sql: `SELECT sequence, operation_sha256, operation_json
|
|
134
|
+
FROM oh_sync_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ?
|
|
135
|
+
ORDER BY sequence LIMIT ?`, args: [bundle.spaceId, first.sequence, last.sequence,
|
|
136
|
+
bundle.operations.length] });
|
|
137
|
+
const rows = result.rows;
|
|
138
|
+
if (!Array.isArray(rows) || rows.length !== bundle.operations.length) {
|
|
139
|
+
return yield* Effect.fail(conflict("Sync conflict: remote history does not contain the exact pushed operations."));
|
|
140
|
+
}
|
|
141
|
+
for (let index = 0; index < bundle.operations.length; index += 1) {
|
|
142
|
+
const operation = bundle.operations[index];
|
|
143
|
+
const row = rows[index];
|
|
144
|
+
if (row === undefined || operation === undefined) {
|
|
145
|
+
return yield* Effect.fail(conflict("Sync conflict: remote history does not contain the exact pushed operations."));
|
|
146
|
+
}
|
|
147
|
+
const sequence = Number(rowValue(row, "sequence", 0));
|
|
148
|
+
const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
|
|
149
|
+
const operationJson = rowValue(row, "operation_json", 2);
|
|
150
|
+
const expectedJson = canonicalJson(operation);
|
|
151
|
+
if (sequence !== operation.sequence || operationSha256 !== operation.operationSha256
|
|
152
|
+
|| typeof operationJson !== "string"
|
|
153
|
+
|| utf8ByteLength(operationJson) > OH_OPERATION_MAX_BYTES_V1
|
|
154
|
+
|| operationJson !== expectedJson) {
|
|
155
|
+
return yield* Effect.fail(conflict("Sync conflict: remote history differs from the pushed operations."));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
|
|
159
|
+
}
|
|
160
|
+
if (first.sequence !== remote.sequence + 1 || first.parentOperationSha256 !== remote.operationSha256) {
|
|
161
|
+
return yield* Effect.fail(conflict("Sync conflict: pushed history does not extend the remote head."));
|
|
162
|
+
}
|
|
163
|
+
yield* client.batch(bundle.operations.map((operation) => ({
|
|
164
|
+
sql: "INSERT INTO oh_sync_operations(space_id, sequence, operation_sha256, operation_json) VALUES (?, ?, ?, ?)",
|
|
165
|
+
args: [bundle.spaceId, operation.sequence, operation.operationSha256, canonicalJson(operation)],
|
|
166
|
+
})), "write");
|
|
167
|
+
return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
|
|
168
|
+
}),
|
|
169
|
+
} satisfies SyncTransportService;
|
|
170
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { canonicalJson } from "./canonical";
|
|
4
|
+
import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
|
|
5
|
+
import { OhSqliteStore } from "./sqlite/store";
|
|
6
|
+
import { createLibSqlOperationSyncTransportV1, synchronizeOhStoreV1,
|
|
7
|
+
type LibSqlClientV1, type OhOperationSyncTransportV1 } from "./sync";
|
|
8
|
+
|
|
9
|
+
function clientWithSetup(batch: LibSqlClientV1["batch"]): LibSqlClientV1 {
|
|
10
|
+
return {
|
|
11
|
+
batch,
|
|
12
|
+
execute: async (statement) => ({ rows: typeof statement !== "string"
|
|
13
|
+
&& statement.sql.includes("SELECT contract_sha256, manifest_json")
|
|
14
|
+
? [{ contract_sha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
15
|
+
manifest_json: canonicalJson(OH_CONTRACT_MANIFEST_V1) }]
|
|
16
|
+
: [] }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("sync Effect lifecycle compatibility", () => {
|
|
21
|
+
test("one failed initialization settles every admitted waiter before a later retry", async () => {
|
|
22
|
+
const gate = Promise.withResolvers<void>();
|
|
23
|
+
const failure = Object.freeze({ code: "synthetic setup refusal" });
|
|
24
|
+
let attempts = 0;
|
|
25
|
+
const transport = createLibSqlOperationSyncTransportV1(clientWithSetup(async () => {
|
|
26
|
+
attempts++;
|
|
27
|
+
if (attempts === 1) { await gate.promise; throw failure; }
|
|
28
|
+
return [];
|
|
29
|
+
}));
|
|
30
|
+
const calls = [transport.handshake(OH_CONTRACT_MANIFEST_V1), transport.head("default"), transport.pull("default", 0, 1)];
|
|
31
|
+
// Attach all rejection observers before releasing the shared attempt.
|
|
32
|
+
const outcomes = Promise.allSettled(calls);
|
|
33
|
+
expect(attempts).toBe(1);
|
|
34
|
+
gate.resolve();
|
|
35
|
+
for (const outcome of await outcomes) {
|
|
36
|
+
expect(outcome.status).toBe("rejected");
|
|
37
|
+
if (outcome.status === "rejected") expect(outcome.reason).toBe(failure);
|
|
38
|
+
}
|
|
39
|
+
expect(attempts).toBe(1);
|
|
40
|
+
expect(await transport.head("default")).toEqual({ operationSha256: null, sequence: 0, v: 1 });
|
|
41
|
+
await transport.handshake(OH_CONTRACT_MANIFEST_V1);
|
|
42
|
+
expect(attempts).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("concurrent successful initialization is performed once", async () => {
|
|
46
|
+
const gate = Promise.withResolvers<void>();
|
|
47
|
+
let attempts = 0;
|
|
48
|
+
const transport = createLibSqlOperationSyncTransportV1(clientWithSetup(async () => {
|
|
49
|
+
attempts++;
|
|
50
|
+
await gate.promise;
|
|
51
|
+
return [];
|
|
52
|
+
}));
|
|
53
|
+
const ready = Promise.all(Array.from({ length: 20 }, () => transport.head("default")));
|
|
54
|
+
expect(attempts).toBe(1);
|
|
55
|
+
gate.resolve();
|
|
56
|
+
expect(await ready).toHaveLength(20);
|
|
57
|
+
expect(attempts).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("the sync facade preserves non-Error transport rejection identity without retry", async () => {
|
|
61
|
+
const store = new OhSqliteStore({ path: ":memory:" });
|
|
62
|
+
const failure = Object.freeze({ code: "synthetic handshake rejection" });
|
|
63
|
+
let calls = 0;
|
|
64
|
+
const transport: OhOperationSyncTransportV1 = {
|
|
65
|
+
handshake: async () => { calls++; throw failure; },
|
|
66
|
+
head: async () => { throw new Error("unexpected head"); },
|
|
67
|
+
pull: async () => { throw new Error("unexpected pull"); },
|
|
68
|
+
push: async () => { throw new Error("unexpected push"); },
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
await expect(synchronizeOhStoreV1(store, transport)).rejects.toBe(failure);
|
|
72
|
+
expect(calls).toBe(1);
|
|
73
|
+
expect(store.head().sequence).toBe(0);
|
|
74
|
+
} finally { store.close(); }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("an eager adapter construction failure still returns a rejected Promise", async () => {
|
|
78
|
+
const store = new OhSqliteStore({ path: ":memory:" });
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(store, "spaceId")!;
|
|
80
|
+
const failure = new Error("synthetic store getter");
|
|
81
|
+
Object.defineProperty(store, "spaceId", { configurable: true, get: () => { throw failure; } });
|
|
82
|
+
try {
|
|
83
|
+
const pending = synchronizeOhStoreV1(store, createLibSqlOperationSyncTransportV1(clientWithSetup(async () => [])));
|
|
84
|
+
expect(pending).toBeInstanceOf(Promise);
|
|
85
|
+
await expect(pending).rejects.toBe(failure);
|
|
86
|
+
} finally {
|
|
87
|
+
Object.defineProperty(store, "spaceId", descriptor);
|
|
88
|
+
store.close();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a defective shared setup response settles waiters and permits a later attempt", async () => {
|
|
93
|
+
const defect = new Error("synthetic response getter defect");
|
|
94
|
+
let attempts = 0;
|
|
95
|
+
const client = clientWithSetup(async () => { attempts++; return []; });
|
|
96
|
+
const execute = client.execute;
|
|
97
|
+
client.execute = async (statement) => {
|
|
98
|
+
if (attempts === 1) return { get rows(): never { throw defect; } };
|
|
99
|
+
return await execute(statement);
|
|
100
|
+
};
|
|
101
|
+
const transport = createLibSqlOperationSyncTransportV1(client);
|
|
102
|
+
const results = await Promise.allSettled([transport.head("default"), transport.handshake(OH_CONTRACT_MANIFEST_V1)]);
|
|
103
|
+
for (const result of results) {
|
|
104
|
+
expect(result.status).toBe("rejected");
|
|
105
|
+
if (result.status === "rejected") expect(result.reason).toBe(defect);
|
|
106
|
+
}
|
|
107
|
+
expect(await transport.head("default")).toMatchObject({ sequence: 0 });
|
|
108
|
+
expect(attempts).toBe(2);
|
|
109
|
+
});
|
|
110
|
+
});
|