@origintrail-official/dkg-storage 10.0.5 → 10.0.7

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.
Files changed (94) hide show
  1. package/README.md +41 -0
  2. package/dist/adapters/blazegraph-nquads.d.ts +36 -0
  3. package/dist/adapters/blazegraph-nquads.d.ts.map +1 -0
  4. package/dist/adapters/blazegraph-nquads.js +79 -0
  5. package/dist/adapters/blazegraph-nquads.js.map +1 -0
  6. package/dist/adapters/blazegraph.d.ts +11 -3
  7. package/dist/adapters/blazegraph.d.ts.map +1 -1
  8. package/dist/adapters/blazegraph.js +241 -84
  9. package/dist/adapters/blazegraph.js.map +1 -1
  10. package/dist/adapters/graph-enumeration-query.d.ts +33 -0
  11. package/dist/adapters/graph-enumeration-query.d.ts.map +1 -0
  12. package/dist/adapters/graph-enumeration-query.js +33 -0
  13. package/dist/adapters/graph-enumeration-query.js.map +1 -0
  14. package/dist/adapters/oxigraph-worker.d.ts +7 -2
  15. package/dist/adapters/oxigraph-worker.d.ts.map +1 -1
  16. package/dist/adapters/oxigraph-worker.js +49 -6
  17. package/dist/adapters/oxigraph-worker.js.map +1 -1
  18. package/dist/adapters/oxigraph.d.ts +7 -2
  19. package/dist/adapters/oxigraph.d.ts.map +1 -1
  20. package/dist/adapters/oxigraph.js +107 -101
  21. package/dist/adapters/oxigraph.js.map +1 -1
  22. package/dist/adapters/sparql-content-types.d.ts +16 -0
  23. package/dist/adapters/sparql-content-types.d.ts.map +1 -0
  24. package/dist/adapters/sparql-content-types.js +16 -0
  25. package/dist/adapters/sparql-content-types.js.map +1 -0
  26. package/dist/adapters/sparql-http.d.ts +17 -2
  27. package/dist/adapters/sparql-http.d.ts.map +1 -1
  28. package/dist/adapters/sparql-http.js +155 -77
  29. package/dist/adapters/sparql-http.js.map +1 -1
  30. package/dist/adapters/sparql-json-results.d.ts +32 -0
  31. package/dist/adapters/sparql-json-results.d.ts.map +1 -0
  32. package/dist/adapters/sparql-json-results.js +37 -0
  33. package/dist/adapters/sparql-json-results.js.map +1 -0
  34. package/dist/atomic-graph-replace.d.ts +33 -0
  35. package/dist/atomic-graph-replace.d.ts.map +1 -0
  36. package/dist/atomic-graph-replace.js +137 -0
  37. package/dist/atomic-graph-replace.js.map +1 -0
  38. package/dist/bounded-rdf.d.ts +45 -0
  39. package/dist/bounded-rdf.d.ts.map +1 -0
  40. package/dist/bounded-rdf.js +291 -0
  41. package/dist/bounded-rdf.js.map +1 -0
  42. package/dist/changelog-store.d.ts +242 -0
  43. package/dist/changelog-store.d.ts.map +1 -0
  44. package/dist/changelog-store.js +761 -0
  45. package/dist/changelog-store.js.map +1 -0
  46. package/dist/graph-knowledge-asset-metadata-loader.d.ts +20 -0
  47. package/dist/graph-knowledge-asset-metadata-loader.d.ts.map +1 -0
  48. package/dist/graph-knowledge-asset-metadata-loader.js +33 -0
  49. package/dist/graph-knowledge-asset-metadata-loader.js.map +1 -0
  50. package/dist/graph-manager.d.ts +170 -0
  51. package/dist/graph-manager.d.ts.map +1 -1
  52. package/dist/graph-manager.js +330 -23
  53. package/dist/graph-manager.js.map +1 -1
  54. package/dist/graph-set-index-store.d.ts +26 -7
  55. package/dist/graph-set-index-store.d.ts.map +1 -1
  56. package/dist/graph-set-index-store.js +280 -53
  57. package/dist/graph-set-index-store.js.map +1 -1
  58. package/dist/graph-write-gen.d.ts +52 -0
  59. package/dist/graph-write-gen.d.ts.map +1 -0
  60. package/dist/graph-write-gen.js +63 -0
  61. package/dist/graph-write-gen.js.map +1 -0
  62. package/dist/http-response-limit.d.ts +9 -0
  63. package/dist/http-response-limit.d.ts.map +1 -0
  64. package/dist/http-response-limit.js +53 -0
  65. package/dist/http-response-limit.js.map +1 -0
  66. package/dist/index.d.ts +14 -6
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +14 -6
  69. package/dist/index.js.map +1 -1
  70. package/dist/local-trusted-controls.d.ts +6 -0
  71. package/dist/local-trusted-controls.d.ts.map +1 -0
  72. package/dist/local-trusted-controls.js +6 -0
  73. package/dist/local-trusted-controls.js.map +1 -0
  74. package/dist/private-store.d.ts +39 -1
  75. package/dist/private-store.d.ts.map +1 -1
  76. package/dist/private-store.js +88 -1
  77. package/dist/private-store.js.map +1 -1
  78. package/dist/shared-memory-literal-blob-store.d.ts +4 -2
  79. package/dist/shared-memory-literal-blob-store.d.ts.map +1 -1
  80. package/dist/shared-memory-literal-blob-store.js +21 -2
  81. package/dist/shared-memory-literal-blob-store.js.map +1 -1
  82. package/dist/store-priority-scheduler.d.ts +49 -9
  83. package/dist/store-priority-scheduler.d.ts.map +1 -1
  84. package/dist/store-priority-scheduler.js +203 -40
  85. package/dist/store-priority-scheduler.js.map +1 -1
  86. package/dist/triple-store.d.ts +78 -3
  87. package/dist/triple-store.d.ts.map +1 -1
  88. package/dist/triple-store.js +90 -2
  89. package/dist/triple-store.js.map +1 -1
  90. package/dist/unsupported-capability-error.d.ts +28 -0
  91. package/dist/unsupported-capability-error.d.ts.map +1 -0
  92. package/dist/unsupported-capability-error.js +34 -0
  93. package/dist/unsupported-capability-error.js.map +1 -0
  94. package/package.json +3 -2
package/README.md CHANGED
@@ -66,6 +66,47 @@ For heavy / production workloads, prefer an out-of-process SPARQL server
66
66
  (`sparql-http` or `blazegraph`), which handles reads and writes concurrently
67
67
  and keeps the daemon responsive under load.
68
68
 
69
+ ## External-store admission and deadlines
70
+
71
+ All external adapters share a process-wide priority scheduler. Waiting work is
72
+ bounded independently for `ack`, `normal`, and `background` traffic, and an
73
+ operation rejected before dispatch receives `StoreSchedulerBusyError` with
74
+ `code: STORE_SCHEDULER_BUSY`, `retryable: true`, and a reason of either
75
+ `queue_full` or `queue_wait_timeout`. Because this error is only created before
76
+ the operation closure starts, retrying it cannot duplicate a write that might
77
+ already have reached the store.
78
+
79
+ | Environment variable | Default | Purpose |
80
+ |---|---:|---|
81
+ | `DKG_STORE_MAX_CONCURRENT` | `8` | Maximum external-store operations in flight. |
82
+ | `DKG_STORE_ACK_RESERVED_SLOTS` | `1` | In-flight capacity reserved for ACK work. |
83
+ | `DKG_STORE_NORMAL_RESERVED_SLOTS` | `1` | Non-ACK capacity kept available for normal work while background operations are in flight. |
84
+ | `DKG_STORE_BACKGROUND_RESERVED_SLOTS` | `1` | Non-ACK capacity reserved for background progress. |
85
+ | `DKG_STORE_QUEUE_LIMIT` | `64` | Maximum waiting operations in each priority queue. |
86
+ | `DKG_STORE_ACK_QUEUE_LIMIT` | common limit | Optional ACK queue override. |
87
+ | `DKG_STORE_NORMAL_QUEUE_LIMIT` | common limit | Optional normal queue override. |
88
+ | `DKG_STORE_BACKGROUND_QUEUE_LIMIT` | common limit | Optional background queue override. |
89
+ | `DKG_STORE_QUEUE_WAIT_TIMEOUT_MS` | `10000` | Maximum pre-dispatch wait before a retryable busy rejection. |
90
+ | `DKG_BLAZEGRAPH_OPERATION_TIMEOUT_MS` | `30000` | Blazegraph end-to-end operation deadline, including scheduler wait, HTTP, response decoding, and mapping. |
91
+
92
+ Normal and background reserves are normalized against the available non-ACK
93
+ capacity. At least one background slot remains available, and the background
94
+ progress floor is capped to its admission ceiling so conflicting custom values
95
+ cannot leave usable capacity idle.
96
+
97
+ Blazegraph's deadline can also be set per store, which takes precedence over
98
+ the environment default:
99
+
100
+ ```jsonc
101
+ "store": {
102
+ "backend": "blazegraph",
103
+ "options": {
104
+ "url": "http://127.0.0.1:9999/blazegraph/namespace/dkg/sparql",
105
+ "timeout": 30000
106
+ }
107
+ }
108
+ ```
109
+
69
110
  ## Internal Dependencies
70
111
 
71
112
  - `@origintrail-official/dkg-core` — configuration types, logging, constants
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Blazegraph N-Quads wire encoder.
3
+ *
4
+ * A focused boundary for the one structural quirk of Blazegraph's bulk-insert
5
+ * endpoint: its N-Quads parser reads the request body BYTE-wise as ASCII (the
6
+ * Content-Type charset is ignored), so any non-ASCII byte is replaced with
7
+ * U+FFFD before parsing — silent stored-value corruption. Downstream, a
8
+ * published KA carrying such a literal fails its storage-ACK merkle
9
+ * recomputation (MERKLE_MISMATCH_IN_SWM) and the publish dies.
10
+ *
11
+ * Verified against the stock `lyrasis/blazegraph:2.1.5` image (image CI + the
12
+ * devnet run). Kept here, separate from the adapter's transport/query/mutation
13
+ * orchestration, so the wire-format invariant is a single testable unit.
14
+ */
15
+ /**
16
+ * Make an assembled N-Quads line ASCII-safe for Blazegraph's bulk-insert
17
+ * endpoint:
18
+ *
19
+ * - Raw non-ASCII UTF-8 (a 2-byte `é` as much as a 4-byte astral emoji) →
20
+ * `\uXXXX` per UTF-16 code unit (astral chars as their surrogate pair) — the
21
+ * exact Java-String form Blazegraph itself emits on CONSTRUCT read-back and
22
+ * the one encoding that round-trips every scalar, including the
23
+ * supplementary plane.
24
+ * - In-range `\UXXXXXXXX` escapes (which Blazegraph truncates to their low 16
25
+ * bits on parse, e.g. `\U0001F600` 😀 → U+F600) → equivalent `\uXXXX`
26
+ * escape(s). Out-of-range `\U` (> U+10FFFF, unrepresentable) passes through.
27
+ *
28
+ * `\uXXXX` is a valid UCHAR in both IRIREF and STRING_LITERAL_QUOTED, so the
29
+ * transform is safe to apply to the whole line. Existing escape sequences are
30
+ * consumed pairwise (backslash parity), so a literal backslash followed by
31
+ * `U…` text (`\\U0001F600`) is never mis-rewritten. The transform is line-level
32
+ * by design — the `\U` normalization and the backslash-parity guard are
33
+ * properties of the rendered line, not of an individual term.
34
+ */
35
+ export declare function toBlazegraphAsciiSafeNQuads(line: string): string;
36
+ //# sourceMappingURL=blazegraph-nquads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blazegraph-nquads.d.ts","sourceRoot":"","sources":["../../src/adapters/blazegraph-nquads.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmChE"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Blazegraph N-Quads wire encoder.
3
+ *
4
+ * A focused boundary for the one structural quirk of Blazegraph's bulk-insert
5
+ * endpoint: its N-Quads parser reads the request body BYTE-wise as ASCII (the
6
+ * Content-Type charset is ignored), so any non-ASCII byte is replaced with
7
+ * U+FFFD before parsing — silent stored-value corruption. Downstream, a
8
+ * published KA carrying such a literal fails its storage-ACK merkle
9
+ * recomputation (MERKLE_MISMATCH_IN_SWM) and the publish dies.
10
+ *
11
+ * Verified against the stock `lyrasis/blazegraph:2.1.5` image (image CI + the
12
+ * devnet run). Kept here, separate from the adapter's transport/query/mutation
13
+ * orchestration, so the wire-format invariant is a single testable unit.
14
+ */
15
+ /**
16
+ * Make an assembled N-Quads line ASCII-safe for Blazegraph's bulk-insert
17
+ * endpoint:
18
+ *
19
+ * - Raw non-ASCII UTF-8 (a 2-byte `é` as much as a 4-byte astral emoji) →
20
+ * `\uXXXX` per UTF-16 code unit (astral chars as their surrogate pair) — the
21
+ * exact Java-String form Blazegraph itself emits on CONSTRUCT read-back and
22
+ * the one encoding that round-trips every scalar, including the
23
+ * supplementary plane.
24
+ * - In-range `\UXXXXXXXX` escapes (which Blazegraph truncates to their low 16
25
+ * bits on parse, e.g. `\U0001F600` 😀 → U+F600) → equivalent `\uXXXX`
26
+ * escape(s). Out-of-range `\U` (> U+10FFFF, unrepresentable) passes through.
27
+ *
28
+ * `\uXXXX` is a valid UCHAR in both IRIREF and STRING_LITERAL_QUOTED, so the
29
+ * transform is safe to apply to the whole line. Existing escape sequences are
30
+ * consumed pairwise (backslash parity), so a literal backslash followed by
31
+ * `U…` text (`\\U0001F600`) is never mis-rewritten. The transform is line-level
32
+ * by design — the `\U` normalization and the backslash-parity guard are
33
+ * properties of the rendered line, not of an individual term.
34
+ */
35
+ export function toBlazegraphAsciiSafeNQuads(line) {
36
+ let out = '';
37
+ for (let i = 0; i < line.length; i++) {
38
+ const code = line.charCodeAt(i);
39
+ if (code === 0x5c /* backslash */) {
40
+ // In-range \UXXXXXXXX → equivalent \uXXXX escape(s) Blazegraph parses
41
+ // correctly. Out-of-range \U (> U+10FFFF, unrepresentable) passes through.
42
+ if (line[i + 1] === 'U') {
43
+ const hex = line.slice(i + 2, i + 10);
44
+ if (/^[0-9A-Fa-f]{8}$/.test(hex)) {
45
+ const cp = parseInt(hex, 16);
46
+ if (cp <= 0x10ffff) {
47
+ out += escapeUtf16CodeUnits(String.fromCodePoint(cp));
48
+ i += 9;
49
+ continue;
50
+ }
51
+ }
52
+ }
53
+ // Any other escape: copy the pair verbatim so the escaped char is never
54
+ // re-inspected (preserves backslash parity for the \U check above).
55
+ out += line[i];
56
+ if (i + 1 < line.length) {
57
+ const nextCode = line.charCodeAt(i + 1);
58
+ out += nextCode >= 0x7f ? escapeUtf16CodeUnits(line[i + 1]) : line[i + 1];
59
+ i++;
60
+ }
61
+ continue;
62
+ }
63
+ if (code >= 0x7f) {
64
+ out += escapeUtf16CodeUnits(line[i]);
65
+ continue;
66
+ }
67
+ out += line[i];
68
+ }
69
+ return out;
70
+ }
71
+ /** `\uXXXX` escape per UTF-16 code unit (surrogate halves stay split). */
72
+ function escapeUtf16CodeUnits(s) {
73
+ let out = '';
74
+ for (let i = 0; i < s.length; i++) {
75
+ out += `\\u${s.charCodeAt(i).toString(16).toUpperCase().padStart(4, '0')}`;
76
+ }
77
+ return out;
78
+ }
79
+ //# sourceMappingURL=blazegraph-nquads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blazegraph-nquads.js","sourceRoot":"","sources":["../../src/adapters/blazegraph-nquads.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,2BAA2B,CAAC,IAAY;IACtD,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,IAAI,KAAK,IAAI,CAAC,eAAe,EAAE,CAAC;YAClC,sEAAsE;YACtE,2EAA2E;YAC3E,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACxB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;gBACtC,IAAI,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;oBACjC,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;oBAC7B,IAAI,EAAE,IAAI,QAAQ,EAAE,CAAC;wBACnB,GAAG,IAAI,oBAAoB,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC;wBACtD,CAAC,IAAI,CAAC,CAAC;wBACP,SAAS;oBACX,CAAC;gBACH,CAAC;YACH,CAAC;YACD,wEAAwE;YACxE,oEAAoE;YACpE,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;YACf,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;gBACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBACxC,GAAG,IAAI,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC1E,CAAC,EAAE,CAAC;YACN,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;YACjB,GAAG,IAAI,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YACrC,SAAS;QACX,CAAC;QACD,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,0EAA0E;AAC1E,SAAS,oBAAoB,CAAC,CAAS;IACrC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,GAAG,IAAI,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IAC7E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -1,4 +1,9 @@
1
- import type { TripleStore, Quad as DKGQuad, QueryOptions, StorePressureSnapshot, TripleStoreQueryOptions, QueryResult } from '../triple-store.js';
1
+ import type { TripleStore, Quad as DKGQuad, QueryOptions, UpdateOptions, StorePressureSnapshot, TripleStoreQueryOptions, QueryResult } from '../triple-store.js';
2
+ export declare const DEFAULT_BLAZEGRAPH_OPERATION_TIMEOUT_MS = 30000;
3
+ export interface BlazegraphStoreOptions {
4
+ /** End-to-end timeout including scheduler wait, HTTP work, and response decoding. */
5
+ timeout?: number;
6
+ }
2
7
  /**
3
8
  * BlazegraphStore — TripleStore adapter backed by a remote Blazegraph
4
9
  * SPARQL endpoint over HTTP. Works with any Blazegraph 2.x instance
@@ -10,7 +15,8 @@ import type { TripleStore, Quad as DKGQuad, QueryOptions, StorePressureSnapshot,
10
15
  export declare class BlazegraphStore implements TripleStore {
11
16
  readonly queryCancellation: "interruptible";
12
17
  private readonly url;
13
- constructor(url: string);
18
+ private readonly operationTimeoutMs;
19
+ constructor(url: string, options?: BlazegraphStoreOptions);
14
20
  private runStoreWork;
15
21
  getPressureSnapshot(): StorePressureSnapshot;
16
22
  insert(quads: DKGQuad[], options?: QueryOptions): Promise<void>;
@@ -27,7 +33,9 @@ export declare class BlazegraphStore implements TripleStore {
27
33
  * delete with two full-graph `countQuads` scans (see above), which is
28
34
  * prohibitive on a CPU-pegged core.
29
35
  */
30
- update(sparql: string, options?: QueryOptions): Promise<void>;
36
+ update(sparql: string, options?: UpdateOptions): Promise<void>;
37
+ replaceGraph(graphUri: string, quads: DKGQuad[], options?: QueryOptions): Promise<void>;
38
+ replaceGraphAndSubject(graphUri: string, graphQuads: DKGQuad[], metaGraphUri: string, metadataSubject: string, metadataQuads: DKGQuad[], options?: QueryOptions): Promise<void>;
31
39
  query(sparql: string, options?: TripleStoreQueryOptions): Promise<QueryResult>;
32
40
  private queryConstruct;
33
41
  hasGraph(graphUri: string, options?: QueryOptions): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"blazegraph.d.ts","sourceRoot":"","sources":["../../src/adapters/blazegraph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,IAAI,IAAI,OAAO,EACf,YAAY,EACZ,qBAAqB,EACrB,uBAAuB,EACvB,WAAW,EAIZ,MAAM,oBAAoB,CAAC;AAM5B;;;;;;;GAOG;AACH,qBAAa,eAAgB,YAAW,WAAW;IACjD,QAAQ,CAAC,iBAAiB,EAAG,eAAe,CAAU;IAEtD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,GAAG,EAAE,MAAM;IAIvB,OAAO,CAAC,YAAY;IAapB,mBAAmB,IAAI,qBAAqB;IAQtC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB/D,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAa/D,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA+BnF,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBtG;;;;;;;;;OASG;IACG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY7D,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC;YAwDtE,cAAc;IAuBtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;IAQpE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAQlE,UAAU,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAahE,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBtE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAQd,YAAY;CAwB3B"}
1
+ {"version":3,"file":"blazegraph.d.ts","sourceRoot":"","sources":["../../src/adapters/blazegraph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,IAAI,IAAI,OAAO,EACf,YAAY,EACZ,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,WAAW,EAIZ,MAAM,oBAAoB,CAAC;AAuB5B,eAAO,MAAM,uCAAuC,QAAS,CAAC;AAE9D,MAAM,WAAW,sBAAsB;IACrC,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAmHD;;;;;;;GAOG;AACH,qBAAa,eAAgB,YAAW,WAAW;IACjD,QAAQ,CAAC,iBAAiB,EAAG,eAAe,CAAU;IAEtD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;gBAEhC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B;IAK7D,OAAO,CAAC,YAAY;IAmCpB,mBAAmB,IAAI,qBAAqB;IAQtC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA4B/D,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAa/D,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA+BnF,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBtG;;;;;;;;;OASG;IACG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9D,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EAAE,EAChB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,IAAI,CAAC;IAwBV,sBAAsB,CAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,OAAO,EAAE,EACrB,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,MAAM,EACvB,aAAa,EAAE,OAAO,EAAE,EACxB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,IAAI,CAAC;IAgCV,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC;YAqDtE,cAAc;IAiCtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;IAQpE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAQlE,UAAU,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAehE,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBtE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAQd,YAAY;CA2B3B"}
@@ -1,7 +1,111 @@
1
+ import { performance } from 'node:perf_hooks';
1
2
  import { registerTripleStoreAdapter } from '../triple-store.js';
2
3
  import { buildBlankNodeSafeDelete } from './sparql-http.js';
3
- import { assertQuadLiteralsMutf8Safe, JAVA_WRITE_UTF_MAX_BYTES } from '@origintrail-official/dkg-core';
4
+ import { formatSparqlJsonBindings, } from './sparql-json-results.js';
5
+ import { toBlazegraphAsciiSafeNQuads } from './blazegraph-nquads.js';
6
+ import { SPARQL_QUERY_CONTENT_TYPE, SPARQL_UPDATE_CONTENT_TYPE } from './sparql-content-types.js';
7
+ import { assertQuadLiteralsMutf8Safe, getMetrics, JAVA_WRITE_UTF_MAX_BYTES, } from '@origintrail-official/dkg-core';
4
8
  import { externalStorePriorityScheduler } from '../store-priority-scheduler.js';
9
+ import { buildAtomicGraphAndSubjectReplaceUpdate, buildAtomicGraphReplaceUpdate, isAtomicGraphReplaceStagingGraph, } from '../atomic-graph-replace.js';
10
+ import { quadToNQuad } from '../bounded-rdf.js';
11
+ import { readResponseTextBounded } from '../http-response-limit.js';
12
+ export const DEFAULT_BLAZEGRAPH_OPERATION_TIMEOUT_MS = 30_000;
13
+ function parsePositiveIntegerEnv(name, fallback) {
14
+ const raw = process.env[name]?.trim();
15
+ if (!raw)
16
+ return fallback;
17
+ const parsed = Number(raw);
18
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
19
+ }
20
+ function resolveOperationTimeout(configured) {
21
+ if (configured === undefined) {
22
+ return parsePositiveIntegerEnv('DKG_BLAZEGRAPH_OPERATION_TIMEOUT_MS', DEFAULT_BLAZEGRAPH_OPERATION_TIMEOUT_MS);
23
+ }
24
+ if (!Number.isInteger(configured) || configured <= 0) {
25
+ throw new Error('BlazegraphStore timeout must be a positive integer in milliseconds');
26
+ }
27
+ return configured;
28
+ }
29
+ function abortError(signal) {
30
+ const reason = signal.reason;
31
+ return reason instanceof Error ? reason : new Error(String(reason ?? 'aborted'));
32
+ }
33
+ function createStoreOperationDeadline(timeoutMs, callerSignal) {
34
+ const controller = new AbortController();
35
+ const deadlineAt = performance.now() + timeoutMs;
36
+ let timer;
37
+ let callerAttached = false;
38
+ const timeoutError = () => new DOMException(`Blazegraph operation exceeded its ${timeoutMs}ms deadline`, 'TimeoutError');
39
+ const detachCaller = () => {
40
+ if (!callerAttached)
41
+ return;
42
+ callerAttached = false;
43
+ callerSignal?.removeEventListener('abort', onCallerAbort);
44
+ };
45
+ const onCallerAbort = () => {
46
+ detachCaller();
47
+ controller.abort(callerSignal?.reason);
48
+ };
49
+ if (callerSignal?.aborted) {
50
+ controller.abort(callerSignal.reason);
51
+ }
52
+ else {
53
+ if (callerSignal) {
54
+ callerAttached = true;
55
+ callerSignal.addEventListener('abort', onCallerAbort, { once: true });
56
+ }
57
+ timer = setTimeout(() => {
58
+ timer = undefined;
59
+ detachCaller();
60
+ controller.abort(timeoutError());
61
+ }, timeoutMs);
62
+ if (typeof timer.unref === 'function')
63
+ timer.unref();
64
+ }
65
+ const check = () => {
66
+ if (controller.signal.aborted)
67
+ throw abortError(controller.signal);
68
+ if (performance.now() < deadlineAt)
69
+ return;
70
+ const error = timeoutError();
71
+ detachCaller();
72
+ controller.abort(error);
73
+ throw error;
74
+ };
75
+ const waitFor = async (work) => {
76
+ let result;
77
+ try {
78
+ // Do not race the operation against the abort signal here. The signal is
79
+ // already passed to fetch (and therefore to its response body), so the
80
+ // underlying promise settles only after the HTTP stack has processed the
81
+ // cancellation. Racing would reject the scheduler task immediately and
82
+ // release its admission slot while the prior Blazegraph request could
83
+ // still be unwinding server-side, allowing a retry to overlap it.
84
+ result = await work;
85
+ }
86
+ catch (error) {
87
+ // The timer callback may be delayed by synchronous response processing.
88
+ // Re-check the monotonic deadline before preserving an underlying error
89
+ // so elapsed operations still surface the deadline's TimeoutError.
90
+ check();
91
+ throw error;
92
+ }
93
+ check();
94
+ return result;
95
+ };
96
+ return {
97
+ signal: controller.signal,
98
+ waitFor,
99
+ check,
100
+ dispose: () => {
101
+ if (timer) {
102
+ clearTimeout(timer);
103
+ timer = undefined;
104
+ }
105
+ detachCaller();
106
+ },
107
+ };
108
+ }
5
109
  /**
6
110
  * BlazegraphStore — TripleStore adapter backed by a remote Blazegraph
7
111
  * SPARQL endpoint over HTTP. Works with any Blazegraph 2.x instance
@@ -13,11 +117,35 @@ import { externalStorePriorityScheduler } from '../store-priority-scheduler.js';
13
117
  export class BlazegraphStore {
14
118
  queryCancellation = 'interruptible';
15
119
  url;
16
- constructor(url) {
120
+ operationTimeoutMs;
121
+ constructor(url, options = {}) {
17
122
  this.url = url.replace(/\/$/, '');
123
+ this.operationTimeoutMs = resolveOperationTimeout(options.timeout);
18
124
  }
19
125
  runStoreWork(operation, options, work) {
20
- return externalStorePriorityScheduler.run(options?.priority, options?.source ?? `blazegraph.${operation}`, work, options?.signal);
126
+ const deadline = createStoreOperationDeadline(this.operationTimeoutMs, options?.signal);
127
+ const source = options?.source ?? `blazegraph.${operation}`;
128
+ let admitted = false;
129
+ const scheduled = externalStorePriorityScheduler.run(options?.priority, source, async () => {
130
+ admitted = true;
131
+ deadline.check();
132
+ const result = await work(deadline);
133
+ deadline.check();
134
+ return result;
135
+ }, deadline.signal);
136
+ return scheduled
137
+ .catch((error) => {
138
+ // This runs only after the admitted work promise has settled. Combined
139
+ // with StoreOperationDeadline.waitFor awaiting the real fetch/body
140
+ // promise, the metric means transport cleanup completed before the
141
+ // caller can launch a retry. Queued work that expired before admission
142
+ // is intentionally excluded.
143
+ if (admitted && deadline.signal.aborted) {
144
+ getMetrics().storeCancellationCompletedTotal.add(1, { operation, source });
145
+ }
146
+ throw error;
147
+ })
148
+ .finally(deadline.dispose);
21
149
  }
22
150
  getPressureSnapshot() {
23
151
  return externalStorePriorityScheduler.snapshot;
@@ -28,24 +156,30 @@ export class BlazegraphStore {
28
156
  async insert(quads, options) {
29
157
  if (quads.length === 0)
30
158
  return;
31
- assertQuadLiteralsMutf8Safe(quads, {
32
- maxBytes: JAVA_WRITE_UTF_MAX_BYTES,
33
- label: 'BlazegraphStore.insert',
34
- });
35
- const nquads = quads.map(quadToNQuad).join('\n') + '\n';
36
- const res = await this.runStoreWork('insert', {
159
+ await this.runStoreWork('insert', {
37
160
  ...options,
38
161
  source: options?.source ?? 'blazegraph.insert',
39
- }, async () => fetch(this.url, {
40
- method: 'POST',
41
- headers: { 'Content-Type': 'text/x-nquads' },
42
- body: nquads,
43
- signal: options?.signal,
44
- }));
45
- if (!res.ok) {
46
- const text = await res.text().catch(() => '');
47
- throw new Error(`Blazegraph insert failed (${res.status}): ${text.slice(0, 200)}`);
48
- }
162
+ }, async (deadline) => {
163
+ assertQuadLiteralsMutf8Safe(quads, {
164
+ maxBytes: JAVA_WRITE_UTF_MAX_BYTES,
165
+ label: 'BlazegraphStore.insert',
166
+ });
167
+ // Serialize only after scheduler admission. A blocked store therefore
168
+ // retains the caller's quad array, not an additional potentially-large
169
+ // N-Quads copy for every waiting insert.
170
+ const nquads = quads.map(quadToBlazegraphNQuad).join('\n') + '\n';
171
+ deadline.check();
172
+ const res = await deadline.waitFor(fetch(this.url, {
173
+ method: 'POST',
174
+ headers: { 'Content-Type': 'text/x-nquads' },
175
+ body: nquads,
176
+ signal: deadline.signal,
177
+ }));
178
+ if (!res.ok) {
179
+ const text = await deadline.waitFor(res.text().catch(() => ''));
180
+ throw new Error(`Blazegraph insert failed (${res.status}): ${text.slice(0, 200)}`);
181
+ }
182
+ });
49
183
  }
50
184
  async delete(quads, options) {
51
185
  if (quads.length === 0)
@@ -109,21 +243,47 @@ export class BlazegraphStore {
109
243
  async update(sparql, options) {
110
244
  await this.sparqlUpdate(sparql, { ...options, source: options?.source ?? 'blazegraph.update' }, 'update');
111
245
  }
246
+ async replaceGraph(graphUri, quads, options) {
247
+ assertQuadLiteralsMutf8Safe(quads, {
248
+ maxBytes: JAVA_WRITE_UTF_MAX_BYTES,
249
+ label: 'BlazegraphStore.replaceGraph',
250
+ });
251
+ const plan = buildAtomicGraphReplaceUpdate(graphUri, quads);
252
+ try {
253
+ await this.sparqlUpdate(plan.update, { ...options, source: options?.source ?? 'blazegraph.replaceGraph' }, 'replaceGraph');
254
+ }
255
+ catch (error) {
256
+ if (plan.cleanup) {
257
+ await this.sparqlUpdate(plan.cleanup, { ...options, source: 'blazegraph.replaceGraph.cleanup' }, 'replaceGraphCleanup').catch(() => undefined);
258
+ }
259
+ throw error;
260
+ }
261
+ }
262
+ async replaceGraphAndSubject(graphUri, graphQuads, metaGraphUri, metadataSubject, metadataQuads, options) {
263
+ assertQuadLiteralsMutf8Safe([...graphQuads, ...metadataQuads], {
264
+ maxBytes: JAVA_WRITE_UTF_MAX_BYTES,
265
+ label: 'BlazegraphStore.replaceGraphAndSubject',
266
+ });
267
+ const plan = buildAtomicGraphAndSubjectReplaceUpdate(graphUri, graphQuads, metaGraphUri, metadataSubject, metadataQuads);
268
+ try {
269
+ await this.sparqlUpdate(plan.update, { ...options, source: options?.source ?? 'blazegraph.replaceGraphAndSubject' }, 'replaceGraphAndSubject');
270
+ }
271
+ catch (error) {
272
+ await this.sparqlUpdate(plan.cleanup, { ...options, source: 'blazegraph.replaceGraphAndSubject.cleanup' }, 'replaceGraphAndSubjectCleanup').catch(() => undefined);
273
+ throw error;
274
+ }
275
+ }
112
276
  // -------------------------------------------------------------------
113
277
  // Queries
114
278
  // -------------------------------------------------------------------
115
279
  async query(sparql, options) {
116
- return this.runStoreWork('query', options, async () => {
117
- if (options?.signal?.aborted) {
118
- const reason = options.signal.reason;
119
- throw reason instanceof Error ? reason : new Error(String(reason ?? 'aborted'));
120
- }
280
+ return this.runStoreWork('query', options, async (deadline) => {
121
281
  const trimmed = sparql.trim();
122
282
  const upper = trimmed.toUpperCase();
123
283
  const isAsk = upper.startsWith('ASK');
124
284
  const isConstruct = upper.startsWith('CONSTRUCT') || upper.startsWith('DESCRIBE');
125
285
  if (isConstruct) {
126
- return this.queryConstruct(trimmed, options);
286
+ return this.queryConstruct(trimmed, deadline, options);
127
287
  }
128
288
  // Direct POST (W3C SPARQL 1.1 Protocol): send the query as the raw
129
289
  // request body with `application/sparql-query` rather than URL-encoded
@@ -132,53 +292,53 @@ export class BlazegraphStore {
132
292
  // on stock Blazegraph) and rejects larger payloads with HTTP 400
133
293
  // "Unable to parse form content". The direct-POST body is not form
134
294
  // parsed, so large queries (e.g. CONSTRUCT/VALUES) are not capped.
135
- const res = await fetch(this.url, {
295
+ // SPARQL_QUERY_CONTENT_TYPE carries charset=utf-8 (see sparql-content-types.ts).
296
+ const res = await deadline.waitFor(fetch(this.url, {
136
297
  method: 'POST',
137
298
  headers: {
138
- 'Content-Type': 'application/sparql-query',
299
+ 'Content-Type': SPARQL_QUERY_CONTENT_TYPE,
139
300
  Accept: 'application/sparql-results+json',
140
301
  },
141
302
  body: trimmed,
142
- signal: options?.signal,
143
- });
303
+ signal: deadline.signal,
304
+ }));
144
305
  if (!res.ok) {
145
- const text = await res.text().catch(() => '');
306
+ const text = await deadline.waitFor((options?.maxResponseBytes === undefined
307
+ ? res.text()
308
+ : readResponseTextBounded(res, options.maxResponseBytes)).catch(() => ''));
146
309
  throw new Error(`Blazegraph query failed (${res.status}): ${text.slice(0, 300)}`);
147
310
  }
148
- const json = (await res.json());
311
+ const json = options?.maxResponseBytes === undefined
312
+ ? await deadline.waitFor(res.json())
313
+ : JSON.parse(await deadline.waitFor(readResponseTextBounded(res, options.maxResponseBytes)));
149
314
  if (isAsk || 'boolean' in json) {
150
315
  return { type: 'boolean', value: json.boolean };
151
316
  }
152
- const sr = json;
153
- const vars = sr.head?.vars ?? [];
154
- const bindings = (sr.results?.bindings ?? []).map((row) => {
155
- const obj = {};
156
- for (const v of vars) {
157
- const cell = row[v];
158
- if (cell)
159
- obj[v] = blazeTermToString(cell);
160
- }
161
- return obj;
162
- });
317
+ const bindings = formatSparqlJsonBindings(json);
163
318
  return { type: 'bindings', bindings };
164
319
  });
165
320
  }
166
- async queryConstruct(sparql, options) {
167
- const res = await fetch(this.url, {
321
+ async queryConstruct(sparql, deadline, options) {
322
+ const res = await deadline.waitFor(fetch(this.url, {
168
323
  method: 'POST',
169
324
  headers: {
170
- 'Content-Type': 'application/sparql-query',
325
+ 'Content-Type': SPARQL_QUERY_CONTENT_TYPE,
171
326
  Accept: 'text/x-nquads, application/n-quads',
172
327
  },
173
328
  body: sparql,
174
- signal: options?.signal,
175
- });
329
+ signal: deadline.signal,
330
+ }));
176
331
  if (!res.ok) {
177
- const text = await res.text().catch(() => '');
332
+ const text = await deadline.waitFor((options?.maxResponseBytes === undefined
333
+ ? res.text()
334
+ : readResponseTextBounded(res, options.maxResponseBytes)).catch(() => ''));
178
335
  throw new Error(`Blazegraph construct failed (${res.status}): ${text.slice(0, 300)}`);
179
336
  }
180
- const text = await res.text();
337
+ const text = await deadline.waitFor(options?.maxResponseBytes === undefined
338
+ ? res.text()
339
+ : readResponseTextBounded(res, options.maxResponseBytes));
181
340
  const quads = parseNQuadsText(text);
341
+ deadline.check();
182
342
  return { type: 'quads', quads };
183
343
  }
184
344
  // -------------------------------------------------------------------
@@ -198,7 +358,9 @@ export class BlazegraphStore {
198
358
  const r = await this.query('SELECT DISTINCT ?g WHERE { GRAPH ?g { ?s ?p ?o } }', options);
199
359
  if (r.type !== 'bindings')
200
360
  return [];
201
- return r.bindings.map((b) => b.g).filter(Boolean);
361
+ return r.bindings
362
+ .map((b) => b.g)
363
+ .filter((graph) => Boolean(graph) && !isAtomicGraphReplaceStagingGraph(graph));
202
364
  }
203
365
  // -------------------------------------------------------------------
204
366
  // Counts
@@ -235,44 +397,38 @@ export class BlazegraphStore {
235
397
  // HTTP 400 "Unable to parse form content" — which broke large publishes
236
398
  // (a publish issues a DELETE DATA / INSERT over the full quad set). The
237
399
  // raw body is not form parsed, so large updates succeed.
238
- const res = await this.runStoreWork(operation, options, async () => fetch(this.url, {
239
- method: 'POST',
240
- headers: { 'Content-Type': 'application/sparql-update' },
241
- body: update,
242
- signal: options?.signal,
243
- }));
244
- if (!res.ok) {
245
- const text = await res.text().catch(() => '');
246
- throw new Error(`Blazegraph update failed (${res.status}): ${text.slice(0, 300)}`);
247
- }
248
- }
249
- }
250
- function escapeNQuadsLiteral(s) {
251
- return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
252
- }
253
- function blazeTermToString(t) {
254
- if (t.type === 'bnode')
255
- return `_:${t.value}`;
256
- if (t.type === 'literal' || t.type === 'typed-literal') {
257
- const escaped = escapeNQuadsLiteral(t.value);
258
- if (t['xml:lang'])
259
- return `"${escaped}"@${t['xml:lang']}`;
260
- if (t.datatype && t.datatype !== 'http://www.w3.org/2001/XMLSchema#string') {
261
- return `"${escaped}"^^<${t.datatype}>`;
262
- }
263
- return `"${escaped}"`;
400
+ //
401
+ // SPARQL_UPDATE_CONTENT_TYPE carries charset=utf-8 (see sparql-content-types.ts).
402
+ await this.runStoreWork(operation, options, async (deadline) => {
403
+ const res = await deadline.waitFor(fetch(this.url, {
404
+ method: 'POST',
405
+ headers: { 'Content-Type': SPARQL_UPDATE_CONTENT_TYPE },
406
+ body: update,
407
+ signal: deadline.signal,
408
+ }));
409
+ if (!res.ok) {
410
+ const text = await deadline.waitFor(res.text().catch(() => ''));
411
+ throw new Error(`Blazegraph update failed (${res.status}): ${text.slice(0, 300)}`);
412
+ }
413
+ });
264
414
  }
265
- return t.value;
266
415
  }
267
416
  // =====================================================================
268
417
  // N-Quad serialisation / parsing helpers (shared with oxigraph adapter)
269
418
  // =====================================================================
270
- function quadToNQuad(q) {
271
- const s = formatTerm(q.subject);
272
- const p = `<${q.predicate}>`;
273
- const o = formatTerm(q.object);
274
- const g = q.graph ? ` <${q.graph}>` : '';
275
- return `${s} ${p} ${o}${g} .`;
419
+ /**
420
+ * N-Quads serializer for Blazegraph's bulk-insert wire format: a standard
421
+ * N-Quads line ({@link quadToNQuad}) made ASCII-safe
422
+ * ({@link toBlazegraphAsciiSafeNQuads}). This is the single serialization
423
+ * entry point for the adapter's write path, so the Blazegraph wire-format
424
+ * invariant lives at the serialization boundary rather than as a separate
425
+ * post-processing pass a caller must remember to apply. The ASCII-safe pass
426
+ * operates on the assembled line by design: it normalizes `\U` escapes and
427
+ * relies on backslash parity across the whole line, both of which are
428
+ * line-level properties of the rendered N-Quads, not of an individual term.
429
+ */
430
+ function quadToBlazegraphNQuad(q) {
431
+ return toBlazegraphAsciiSafeNQuads(quadToNQuad(q));
276
432
  }
277
433
  function formatTerm(term) {
278
434
  if (term.startsWith('"')) {
@@ -394,6 +550,7 @@ registerTripleStoreAdapter('blazegraph', async (opts) => {
394
550
  const url = opts?.url;
395
551
  if (!url)
396
552
  throw new Error('blazegraph adapter requires options.url (SPARQL endpoint)');
397
- return new BlazegraphStore(url);
553
+ const timeout = opts?.timeout;
554
+ return new BlazegraphStore(url, { timeout });
398
555
  });
399
556
  //# sourceMappingURL=blazegraph.js.map