@littlebigbrain/client 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/client.d.ts +27 -3
- package/dist/client.js +162 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/schema.d.ts +826 -107
- package/dist/transport.d.ts +9 -2
- package/dist/transport.js +9 -0
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,21 @@ await graph.facts.import(
|
|
|
78
78
|
);
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
For large or long-running loads, stream records to a durable job instead:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const accepted = await lbb.submitImport(records(), {
|
|
85
|
+
idempotencyKey: "hubspot:portal-42:run-2026-07-29",
|
|
86
|
+
});
|
|
87
|
+
const completed = await lbb.waitForImportJob(accepted.job_id);
|
|
88
|
+
console.log(completed.state, completed.committed_commit_seq);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`records()` may be an iterable or async iterable. Success means every grouped
|
|
92
|
+
commit is durable and final publication was enqueued; it does not mean indexes
|
|
93
|
+
have already reached `committed_commit_seq`. An empty iterable is rejected
|
|
94
|
+
locally before an import POST is sent.
|
|
95
|
+
|
|
81
96
|
**Time-travel read.** Pin any search to a past instant — results reflect the graph as it was then:
|
|
82
97
|
|
|
83
98
|
```ts
|
package/dist/client.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { ImportLine, LbbClientOptions, ListResponse, RawLbbResponse, ReadConsistencyOptions, RdfImportOptions, Schemas, SearchConsistency, SparqlResults } from "./types.js";
|
|
1
|
+
import type { DurableImportSource, ImportLine, LbbClientOptions, ListResponse, RawLbbResponse, ReadConsistencyOptions, RdfImportOptions, Schemas, SearchConsistency, SparqlResults } from "./types.js";
|
|
2
2
|
import { type CallOptions, type RequestOptions } from "./transport.js";
|
|
3
3
|
import { ContextNamespace, EntityNamespace, GraphNamespace, OntologyNamespace, QueryNamespace, SchemaNamespace, SearchNamespace } from "./namespaces.js";
|
|
4
4
|
export { parseSparqlResults } from "./types.js";
|
|
5
|
-
export type { AttributeFilter, AttributeFilterOp, AttributeFilterValue, EntityAttributeFilterOptions, EntityPropertiesLine, FetchLike, FlatProperties, ImportLine, LbbClientOptions, LbbRequestEvent, LbbResponseEvent, LbbRetryEvent, LbbErrorPayload, ListResponse, RawLbbResponse, ReadConsistencyOptions, RdfImportOptions, Schemas, SearchConsistency, SparqlResults, SparqlResultsJson, SparqlTerm, CommitRequest, CommitResponse, Entity, EntitySelector, GraphMetadata, GraphSummary, SchemaView, SearchRequest, SearchResponse, SearchResult, Snapshot, } from "./types.js";
|
|
6
|
-
export { LbbError } from "./transport.js";
|
|
5
|
+
export type { AttributeFilter, AttributeFilterOp, AttributeFilterValue, EntityAttributeFilterOptions, EntityPropertiesLine, DurableImportLine, DurableImportSource, FetchLike, FlatProperties, ImportLine, LbbClientOptions, LbbRequestEvent, LbbResponseEvent, LbbRetryEvent, LbbErrorPayload, ListResponse, RawLbbResponse, ReadConsistencyOptions, RdfImportOptions, Schemas, SearchConsistency, SparqlResults, SparqlResultsJson, SparqlTerm, CommitRequest, CommitResponse, Entity, EntitySelector, GraphMetadata, GraphSummary, SchemaView, SearchRequest, SearchResponse, SearchResult, Snapshot, } from "./types.js";
|
|
6
|
+
export { LbbCapabilityError, LbbError } from "./transport.js";
|
|
7
7
|
export type { CallOptions, Query, QueryValue, RequestOptions, } from "./transport.js";
|
|
8
8
|
export type { HybridSearchOptions } from "./namespaces.js";
|
|
9
9
|
export { ContextNamespace, EntityNamespace, FactsNamespace, GraphNamespace, OntologyNamespace, QueryNamespace, SchemaNamespace, SearchNamespace, } from "./namespaces.js";
|
|
@@ -36,6 +36,7 @@ export declare class LbbClient {
|
|
|
36
36
|
private readonly onRequest?;
|
|
37
37
|
private readonly onResponse?;
|
|
38
38
|
private readonly onRetry?;
|
|
39
|
+
private capabilities?;
|
|
39
40
|
/** A5 default read consistency applied when a read omits its own value. */
|
|
40
41
|
readonly defaultConsistency?: SearchConsistency;
|
|
41
42
|
readonly context: ContextNamespace;
|
|
@@ -75,6 +76,7 @@ export declare class LbbClient {
|
|
|
75
76
|
request<T>(method: string, path: string, opts?: RequestOptions): Promise<T>;
|
|
76
77
|
private mutationKey;
|
|
77
78
|
idempotencyKey(prefix?: string): string;
|
|
79
|
+
private requireCapability;
|
|
78
80
|
/** Commit triplets and optional entity embeddings. Prefer `client.graph("main").facts.create(...)`. */
|
|
79
81
|
commit(body: Schemas["TripletCommitFile"], opts?: {
|
|
80
82
|
idempotencyKey?: string;
|
|
@@ -109,6 +111,28 @@ export declare class LbbClient {
|
|
|
109
111
|
}): Promise<Schemas["GraphImportResponse"] & {
|
|
110
112
|
commitSeq: number | null;
|
|
111
113
|
}>;
|
|
114
|
+
/**
|
|
115
|
+
* Stream NDJSON into immutable storage and enqueue a durable import job.
|
|
116
|
+
*
|
|
117
|
+
* The idempotency key is mandatory and binds the key to the uploaded content.
|
|
118
|
+
* Streaming uploads are attempted once: a one-shot async iterator cannot be
|
|
119
|
+
* replayed safely by an automatic HTTP retry. Call this method again with a
|
|
120
|
+
* fresh iterable and the same key to perform an explicit idempotent replay.
|
|
121
|
+
*/
|
|
122
|
+
submitImport(lines: DurableImportSource, opts: {
|
|
123
|
+
idempotencyKey: string;
|
|
124
|
+
batch?: number;
|
|
125
|
+
strict?: boolean;
|
|
126
|
+
observedAt?: string;
|
|
127
|
+
signal?: AbortSignal;
|
|
128
|
+
}): Promise<Schemas["GraphImportJobAccepted"]>;
|
|
129
|
+
getImportJob(jobId: string): Promise<Schemas["GraphImportJobStatus"]>;
|
|
130
|
+
cancelImportJob(jobId: string): Promise<Schemas["GraphImportJobCancelResponse"]>;
|
|
131
|
+
waitForImportJob(jobId: string, opts?: {
|
|
132
|
+
pollIntervalMs?: number;
|
|
133
|
+
timeoutMs?: number;
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
}): Promise<Schemas["GraphImportJobStatus"]>;
|
|
112
136
|
/**
|
|
113
137
|
* Bulk-ingest N-Triples, Turtle, N-Quads, or TriG without client-side conversion. Resource-object
|
|
114
138
|
* triples become keyed Resource edges; literal-object triples become text
|
package/dist/client.js
CHANGED
|
@@ -1,9 +1,87 @@
|
|
|
1
1
|
import { parseSparqlResults } from "./types.js";
|
|
2
2
|
import { bodyMarksTerminal, errorCodeFromBody, fullJitterBackoffMs, parseLbbError, parseResponseJson, retryAllowed, retryableStatus, retryDelayMs, sleep, } from "./transport.js";
|
|
3
|
+
import { LbbCapabilityError } from "./transport.js";
|
|
3
4
|
import { ContextNamespace, EntityNamespace, GraphNamespace, OntologyNamespace, QueryNamespace, SchemaNamespace, SearchNamespace, } from "./namespaces.js";
|
|
4
5
|
export { parseSparqlResults } from "./types.js";
|
|
5
|
-
export { LbbError } from "./transport.js";
|
|
6
|
+
export { LbbCapabilityError, LbbError } from "./transport.js";
|
|
6
7
|
export { ContextNamespace, EntityNamespace, FactsNamespace, GraphNamespace, OntologyNamespace, QueryNamespace, SchemaNamespace, SearchNamespace, } from "./namespaces.js";
|
|
8
|
+
function durableImportBytes(line) {
|
|
9
|
+
if (line instanceof Uint8Array)
|
|
10
|
+
return line;
|
|
11
|
+
const encoded = typeof line === "string" ? line : JSON.stringify(line);
|
|
12
|
+
return new TextEncoder().encode(encoded.endsWith("\n") ? encoded : `${encoded}\n`);
|
|
13
|
+
}
|
|
14
|
+
function durableImportIterator(source) {
|
|
15
|
+
if (typeof source === "string") {
|
|
16
|
+
return (async function* () {
|
|
17
|
+
yield source;
|
|
18
|
+
})();
|
|
19
|
+
}
|
|
20
|
+
if (Symbol.asyncIterator in Object(source)) {
|
|
21
|
+
return source[Symbol.asyncIterator]();
|
|
22
|
+
}
|
|
23
|
+
const iterator = source[Symbol.iterator]();
|
|
24
|
+
return {
|
|
25
|
+
next: async () => iterator.next(),
|
|
26
|
+
return: async () => {
|
|
27
|
+
iterator.return?.();
|
|
28
|
+
return { done: true, value: undefined };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function durableImportBody(source) {
|
|
33
|
+
const iterator = durableImportIterator(source);
|
|
34
|
+
const first = await iterator.next();
|
|
35
|
+
if (first.done) {
|
|
36
|
+
await iterator.return?.();
|
|
37
|
+
throw new TypeError("submitImport requires at least one NDJSON record or byte chunk");
|
|
38
|
+
}
|
|
39
|
+
let firstPending = true;
|
|
40
|
+
const next = async () => {
|
|
41
|
+
if (firstPending) {
|
|
42
|
+
firstPending = false;
|
|
43
|
+
return { done: false, value: first.value };
|
|
44
|
+
}
|
|
45
|
+
return iterator.next();
|
|
46
|
+
};
|
|
47
|
+
const Stream = globalThis.ReadableStream;
|
|
48
|
+
if (Stream) {
|
|
49
|
+
return new Stream({
|
|
50
|
+
async pull(controller) {
|
|
51
|
+
try {
|
|
52
|
+
const item = await next();
|
|
53
|
+
if (item.done) {
|
|
54
|
+
controller.close();
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
controller.enqueue(durableImportBytes(item.value));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
controller.error(error);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
async cancel() {
|
|
65
|
+
await iterator.return?.();
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
async *[Symbol.asyncIterator]() {
|
|
71
|
+
try {
|
|
72
|
+
for (;;) {
|
|
73
|
+
const item = await next();
|
|
74
|
+
if (item.done)
|
|
75
|
+
return;
|
|
76
|
+
yield durableImportBytes(item.value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await iterator.return?.();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
7
85
|
/**
|
|
8
86
|
* A typed HTTP client for a little big brain graph server. One instance is scoped to a
|
|
9
87
|
* single graph/branch; construct another for a different scope. All methods
|
|
@@ -24,6 +102,7 @@ export class LbbClient {
|
|
|
24
102
|
onRequest;
|
|
25
103
|
onResponse;
|
|
26
104
|
onRetry;
|
|
105
|
+
capabilities;
|
|
27
106
|
/** A5 default read consistency applied when a read omits its own value. */
|
|
28
107
|
defaultConsistency;
|
|
29
108
|
context;
|
|
@@ -172,6 +251,7 @@ export class LbbClient {
|
|
|
172
251
|
method,
|
|
173
252
|
headers,
|
|
174
253
|
body,
|
|
254
|
+
...(opts.duplex ? { duplex: opts.duplex } : {}),
|
|
175
255
|
};
|
|
176
256
|
const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
|
|
177
257
|
const maxRetries = opts.maxRetries ?? this.maxRetries;
|
|
@@ -207,10 +287,16 @@ export class LbbClient {
|
|
|
207
287
|
maxAttempts: maxRetries + 1,
|
|
208
288
|
idempotencyKey: opts.idempotencyKey,
|
|
209
289
|
});
|
|
210
|
-
|
|
290
|
+
const requestInit = {
|
|
211
291
|
...init,
|
|
212
292
|
signal: controller?.signal ?? opts.signal,
|
|
213
|
-
}
|
|
293
|
+
};
|
|
294
|
+
// Keep FetchLike's long-standing string-body test-double contract while
|
|
295
|
+
// allowing this one endpoint to pass a native streaming body to fetch.
|
|
296
|
+
// Native browser/Node fetch implementations accept the extended shape;
|
|
297
|
+
// custom transports that need durable imports can inspect it at runtime.
|
|
298
|
+
const streamingFetch = this.fetchImpl;
|
|
299
|
+
response = await streamingFetch(url, requestInit);
|
|
214
300
|
text = await response.text();
|
|
215
301
|
}
|
|
216
302
|
catch (error) {
|
|
@@ -321,6 +407,12 @@ export class LbbClient {
|
|
|
321
407
|
idempotencyKey(prefix = "request") {
|
|
322
408
|
return this.mutationKey(prefix);
|
|
323
409
|
}
|
|
410
|
+
async requireCapability(capability) {
|
|
411
|
+
this.capabilities ??= this.request("GET", "/version").then((version) => new Set(version.capabilities));
|
|
412
|
+
if (!(await this.capabilities).has(capability)) {
|
|
413
|
+
throw new LbbCapabilityError(capability);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
324
416
|
// --- writes ---
|
|
325
417
|
/** Commit triplets and optional entity embeddings. Prefer `client.graph("main").facts.create(...)`. */
|
|
326
418
|
async commit(body, opts = {}) {
|
|
@@ -375,6 +467,73 @@ export class LbbClient {
|
|
|
375
467
|
// write→floor→read loop reads naturally after a bulk load.
|
|
376
468
|
return { ...response, commitSeq: response.committed_commit_seq ?? null };
|
|
377
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Stream NDJSON into immutable storage and enqueue a durable import job.
|
|
472
|
+
*
|
|
473
|
+
* The idempotency key is mandatory and binds the key to the uploaded content.
|
|
474
|
+
* Streaming uploads are attempted once: a one-shot async iterator cannot be
|
|
475
|
+
* replayed safely by an automatic HTTP retry. Call this method again with a
|
|
476
|
+
* fresh iterable and the same key to perform an explicit idempotent replay.
|
|
477
|
+
*/
|
|
478
|
+
async submitImport(lines, opts) {
|
|
479
|
+
if (!opts?.idempotencyKey?.trim()) {
|
|
480
|
+
throw new TypeError("submitImport requires a non-empty idempotencyKey");
|
|
481
|
+
}
|
|
482
|
+
await this.requireCapability("durable_import_jobs_v1");
|
|
483
|
+
const rawBody = await durableImportBody(lines);
|
|
484
|
+
return this.request("POST", "/v1/graph/import-jobs", {
|
|
485
|
+
rawBody,
|
|
486
|
+
contentType: "application/x-ndjson",
|
|
487
|
+
duplex: "half",
|
|
488
|
+
query: {
|
|
489
|
+
batch: opts.batch,
|
|
490
|
+
strict: opts.strict,
|
|
491
|
+
observed_at: opts.observedAt,
|
|
492
|
+
},
|
|
493
|
+
idempotencyKey: opts.idempotencyKey,
|
|
494
|
+
maxRetries: 0,
|
|
495
|
+
retry: false,
|
|
496
|
+
signal: opts.signal,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async getImportJob(jobId) {
|
|
500
|
+
await this.requireCapability("durable_import_jobs_v1");
|
|
501
|
+
return this.request("GET", "/v1/graph/import-jobs", {
|
|
502
|
+
query: { job_id: jobId },
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async cancelImportJob(jobId) {
|
|
506
|
+
await this.requireCapability("durable_import_jobs_v1");
|
|
507
|
+
return this.request("DELETE", "/v1/graph/import-jobs", {
|
|
508
|
+
query: { job_id: jobId },
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
async waitForImportJob(jobId, opts = {}) {
|
|
512
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 1_000;
|
|
513
|
+
const timeoutMs = opts.timeoutMs ?? 0;
|
|
514
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 0) {
|
|
515
|
+
throw new RangeError("pollIntervalMs must be a non-negative number");
|
|
516
|
+
}
|
|
517
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
518
|
+
throw new RangeError("timeoutMs must be a non-negative number");
|
|
519
|
+
}
|
|
520
|
+
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : undefined;
|
|
521
|
+
for (;;) {
|
|
522
|
+
if (opts.signal?.aborted) {
|
|
523
|
+
throw opts.signal.reason ?? new Error("request aborted");
|
|
524
|
+
}
|
|
525
|
+
const status = await this.getImportJob(jobId);
|
|
526
|
+
if (status.state === "succeeded" ||
|
|
527
|
+
status.state === "failed" ||
|
|
528
|
+
status.state === "cancelled") {
|
|
529
|
+
return status;
|
|
530
|
+
}
|
|
531
|
+
if (deadline !== undefined && Date.now() >= deadline) {
|
|
532
|
+
throw new Error(`timed out waiting for durable import job ${jobId}`);
|
|
533
|
+
}
|
|
534
|
+
await sleep(pollIntervalMs);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
378
537
|
/**
|
|
379
538
|
* Bulk-ingest N-Triples, Turtle, N-Quads, or TriG without client-side conversion. Resource-object
|
|
380
539
|
* triples become keyed Resource edges; literal-object triples become text
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { LbbClient, LbbError, parseSparqlResults } from "./client.js";
|
|
2
|
-
export type { LbbClientOptions, CallOptions, RequestOptions, HybridSearchOptions, LbbRequestEvent, LbbResponseEvent, FetchLike, ReadConsistencyOptions, SearchConsistency, Schemas, SparqlResults, SparqlResultsJson, SparqlTerm, CommitRequest, CommitResponse, Entity, EntitySelector, GraphMetadata, GraphSummary, SchemaView, SearchRequest, SearchResponse, SearchResult, Snapshot, } from "./client.js";
|
|
1
|
+
export { LbbCapabilityError, LbbClient, LbbError, parseSparqlResults, } from "./client.js";
|
|
2
|
+
export type { DurableImportLine, DurableImportSource, LbbClientOptions, CallOptions, RequestOptions, HybridSearchOptions, LbbRequestEvent, LbbResponseEvent, FetchLike, ReadConsistencyOptions, SearchConsistency, Schemas, SparqlResults, SparqlResultsJson, SparqlTerm, CommitRequest, CommitResponse, Entity, EntitySelector, GraphMetadata, GraphSummary, SchemaView, SearchRequest, SearchResponse, SearchResult, Snapshot, } from "./client.js";
|
|
3
3
|
export type { components, paths, operations } from "./schema.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { LbbClient, LbbError, parseSparqlResults } from "./client.js";
|
|
1
|
+
export { LbbCapabilityError, LbbClient, LbbError, parseSparqlResults, } from "./client.js";
|