@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 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
- response = await this.fetchImpl(url, {
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";