@qaecy/cue-cli 0.0.32 → 0.0.33

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.
File without changes
@@ -1,5 +1,27 @@
1
1
  /* @ts-self-types="./dir_scanner_wasm.d.ts" */
2
2
 
3
+ /**
4
+ * Attempts to extract the unit count from any supported file and returns a plain JS
5
+ * object `{ units: number, error: string | null }`.
6
+ *
7
+ * `original_path` is used only for extension detection.
8
+ *
9
+ * ```js
10
+ * const buf = await file.arrayBuffer();
11
+ * const result = debug_file(file.name, new Uint8Array(buf));
12
+ * console.log(result); // { units: 12, error: null }
13
+ * ```
14
+ * @param {string} original_path
15
+ * @param {Uint8Array} data
16
+ * @returns {any}
17
+ */
18
+ export function debug_file(original_path, data) {
19
+ const ptr0 = passStringToWasm0(original_path, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
20
+ const len0 = WASM_VECTOR_LEN;
21
+ const ret = wasm.debug_file(ptr0, len0, data);
22
+ return ret;
23
+ }
24
+
3
25
  /**
4
26
  * Scans a list of in-memory files and returns per-extension metrics sorted by
5
27
  * file count descending.
@@ -101,6 +123,9 @@ function __wbg_get_imports() {
101
123
  const ret = arg0.length;
102
124
  return ret;
103
125
  },
126
+ __wbg_log_7e1aa9064a1dbdbd: function(arg0) {
127
+ console.log(arg0);
128
+ },
104
129
  __wbg_new_0c7403db6e782f19: function(arg0) {
105
130
  const ret = new Uint8Array(arg0);
106
131
  return ret;
@@ -126,6 +151,9 @@ function __wbg_get_imports() {
126
151
  __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
127
152
  arg0[arg1] = arg2;
128
153
  },
154
+ __wbg_warn_3cc416af27dbdc02: function(arg0) {
155
+ console.warn(arg0);
156
+ },
129
157
  __wbindgen_cast_0000000000000001: function(arg0) {
130
158
  // Cast intrinsic for `F64 -> Externref`.
131
159
  const ret = arg0;
package/main.js CHANGED
@@ -3827,19 +3827,6 @@ function generateFileUUID(filepath, providerId = "") {
3827
3827
  return contextBasedGuid(`${providerId}${filepath}`);
3828
3828
  }
3829
3829
 
3830
- // libs/js/id-builders/src/lib/id-extractor.ts
3831
- var extractIdsFromRawPath = (filePath) => {
3832
- if (filePath.startsWith("/")) {
3833
- filePath = filePath.slice(1);
3834
- }
3835
- const pathParts = filePath.split("/");
3836
- const projectId = pathParts[0];
3837
- const fileName = pathParts.pop() ?? "";
3838
- const suffix = `.${fileName.split(".").pop()?.toLowerCase()}`;
3839
- const documentUUID = fileName.replace(/\.[^.]+$/, "");
3840
- return { projectId, documentUUID, suffix };
3841
- };
3842
-
3843
3830
  // libs/js/id-builders/src/lib/md5-builder.ts
3844
3831
  var import_spark_md5 = __toESM(require("spark-md5"));
3845
3832
  async function fromReadStream(readStream) {
@@ -4154,6 +4141,7 @@ var CueAuth = class {
4154
4141
  // libs/js/cue-sdk/src/lib/api.ts
4155
4142
  var ENDPOINT_SEARCH = "/assistant/search";
4156
4143
  var ENDPOINT_SPARQL = "/triplestore/query";
4144
+ var ENDPOINT_CONSUMPTION = "/data-views/admin/consumption/units";
4157
4145
  var CueApi = class {
4158
4146
  constructor(_auth, _gatewayUrl, projects, sync) {
4159
4147
  this._auth = _auth;
@@ -4213,6 +4201,16 @@ var CueApi = class {
4213
4201
  }
4214
4202
  return response.json();
4215
4203
  }
4204
+ async getConsumption(projectId) {
4205
+ const headers = await this._authHeaders();
4206
+ const response = await fetch(`${this._gatewayUrl}${ENDPOINT_CONSUMPTION}`, {
4207
+ headers: { ...headers, "x-project-id": projectId }
4208
+ });
4209
+ if (!response.ok) {
4210
+ throw new Error(`Failed to fetch consumption: ${response.status} ${response.statusText}`);
4211
+ }
4212
+ return response.json();
4213
+ }
4216
4214
  };
4217
4215
 
4218
4216
  // libs/js/cue-sdk/src/lib/project.ts
@@ -4599,9 +4597,18 @@ var Fuseki = class _Fuseki {
4599
4597
  };
4600
4598
 
4601
4599
  // libs/js/databases/src/lib/graph/qlever.ts
4600
+ var import_zlib = require("zlib");
4601
+ var import_n3 = require("n3");
4602
+ var QLeverLockedError = class extends Error {
4603
+ constructor(body) {
4604
+ super(`QLever is locked (rebuild in progress): ${body}`);
4605
+ this.name = "QLeverLockedError";
4606
+ }
4607
+ };
4602
4608
  var QLever = class _QLever {
4603
4609
  queryEndpoint;
4604
4610
  updateEndpoint;
4611
+ dataEndpoint;
4605
4612
  baseHeaders;
4606
4613
  static RELEVANT_HEADER_KEYS = [
4607
4614
  "authorization",
@@ -4610,14 +4617,44 @@ var QLever = class _QLever {
4610
4617
  "x-user-roles"
4611
4618
  // add more if needed
4612
4619
  ];
4620
+ /** Max retries on 423 Locked (rebuild in progress). */
4621
+ static LOCKED_MAX_RETRIES = parseInt(process.env["QLEVER_LOCKED_MAX_RETRIES"] ?? "10", 10);
4622
+ /** Base delay (ms) for exponential backoff on 423. */
4623
+ static LOCKED_BASE_DELAY_MS = parseInt(process.env["QLEVER_LOCKED_BASE_DELAY_MS"] ?? "2000", 10);
4624
+ /**
4625
+ * Retry an async write operation on 423 Locked with exponential backoff + jitter.
4626
+ * 423 means qlever accessor has an ongoing rebuild; we should wait and retry.
4627
+ */
4628
+ static async _retryOnLocked(fn) {
4629
+ const maxRetries = _QLever.LOCKED_MAX_RETRIES;
4630
+ const baseDelayMs = _QLever.LOCKED_BASE_DELAY_MS;
4631
+ let lastError;
4632
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
4633
+ try {
4634
+ return await fn();
4635
+ } catch (err) {
4636
+ lastError = err;
4637
+ if (err instanceof QLeverLockedError && attempt < maxRetries) {
4638
+ const jitter = 0.5 + Math.random();
4639
+ const delay = baseDelayMs * Math.pow(2, attempt) * jitter;
4640
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
4641
+ continue;
4642
+ }
4643
+ throw err;
4644
+ }
4645
+ }
4646
+ throw lastError;
4647
+ }
4613
4648
  constructor(graphOptions) {
4614
4649
  this.queryEndpoint = graphOptions.queryEndpoint;
4615
4650
  this.updateEndpoint = graphOptions.updateEndpoint;
4651
+ this.dataEndpoint = this.updateEndpoint.replace(/\/update$/, "/data");
4616
4652
  this.baseHeaders = Object.fromEntries(
4617
4653
  Object.entries(graphOptions.originalHeaders || {}).filter(
4618
4654
  ([key]) => _QLever.RELEVANT_HEADER_KEYS.includes(key)
4619
4655
  )
4620
4656
  );
4657
+ this.baseHeaders["x-user-roles"] = "admin";
4621
4658
  }
4622
4659
  async ping() {
4623
4660
  const query3 = "ASK { }";
@@ -4627,7 +4664,6 @@ var QLever = class _QLever {
4627
4664
  async query(query3, accept = "application/sparql-results+json") {
4628
4665
  let res;
4629
4666
  try {
4630
- console.log(this.queryEndpoint);
4631
4667
  res = await fetch(this.queryEndpoint, {
4632
4668
  headers: {
4633
4669
  ...this.baseHeaders,
@@ -4661,19 +4697,65 @@ var QLever = class _QLever {
4661
4697
  return await res.text();
4662
4698
  }
4663
4699
  async update(update) {
4664
- const res = await fetch(this.updateEndpoint, {
4665
- headers: {
4666
- ...this.baseHeaders,
4667
- "Content-Type": "application/x-www-form-urlencoded"
4668
- },
4669
- method: "POST",
4670
- body: new URLSearchParams({ update })
4700
+ return _QLever._retryOnLocked(async () => {
4701
+ const res = await fetch(this.updateEndpoint, {
4702
+ headers: {
4703
+ ...this.baseHeaders,
4704
+ "Content-Type": "application/x-www-form-urlencoded"
4705
+ },
4706
+ method: "POST",
4707
+ body: new URLSearchParams({ update })
4708
+ });
4709
+ if (!res.ok) {
4710
+ const body = await res.text();
4711
+ if (res.status === 423)
4712
+ throw new QLeverLockedError(body);
4713
+ throw new Error(`SPARQL update failed (HTTP ${res.status}): ${body}`);
4714
+ }
4715
+ return await res.json();
4716
+ });
4717
+ }
4718
+ /**
4719
+ * Insert quads via the /data endpoint, grouped by named graph.
4720
+ * This is preferred over SPARQL INSERT DATA for QLever because
4721
+ * the /data endpoint correctly registers named graphs in the index.
4722
+ */
4723
+ async insertData(quads) {
4724
+ await this._postToDataEndpoint(quads, this.dataEndpoint);
4725
+ }
4726
+ /**
4727
+ * Delete quads via the /data/delete endpoint, grouped by named graph.
4728
+ */
4729
+ async deleteData(quads) {
4730
+ await this._postToDataEndpoint(quads, `${this.dataEndpoint}/delete`);
4731
+ }
4732
+ async _postToDataEndpoint(quads, baseUrl) {
4733
+ const nquads = await this._quadsToNQuads(quads);
4734
+ const body = new Uint8Array((0, import_zlib.gzipSync)(Buffer.from(nquads, "utf-8")));
4735
+ await _QLever._retryOnLocked(async () => {
4736
+ const res = await fetch(baseUrl, {
4737
+ method: "POST",
4738
+ headers: {
4739
+ ...this.baseHeaders,
4740
+ "Content-Type": "application/n-quads",
4741
+ "Content-Encoding": "gzip"
4742
+ },
4743
+ body
4744
+ });
4745
+ if (!res.ok) {
4746
+ const text = await res.text();
4747
+ if (res.status === 423)
4748
+ throw new QLeverLockedError(text);
4749
+ throw new Error(`QLever data POST failed (HTTP ${res.status}): ${text}`);
4750
+ }
4751
+ });
4752
+ }
4753
+ _quadsToNQuads(quads) {
4754
+ return new Promise((resolve2, reject) => {
4755
+ const writer = new import_n3.Writer({ format: "application/n-quads" });
4756
+ writer.addQuads(quads);
4757
+ writer.end((err, result) => err ? reject(err) : resolve2(result));
4671
4758
  });
4672
- if (!res.ok) {
4673
- const body = await res.text();
4674
- throw new Error(`SPARQL update failed (HTTP ${res.status}): ${body}`);
4675
- }
4676
- return await res.json();
4677
4759
  }
4678
4760
  };
4679
4761
 
@@ -4718,6 +4800,32 @@ var CueGraphDatabase = class {
4718
4800
  update(updateString) {
4719
4801
  return this._db.update(updateString);
4720
4802
  }
4803
+ /** Returns true if this backend supports the /data bulk-insert endpoint (QLever only). */
4804
+ supportsDataEndpoint() {
4805
+ return this.options.graphType === "qlever";
4806
+ }
4807
+ /**
4808
+ * Insert quads using the backend's preferred bulk-insert mechanism.
4809
+ * For QLever: uses the /data endpoint (correctly registers named graphs).
4810
+ * For Fuseki: falls back to SPARQL INSERT DATA.
4811
+ */
4812
+ insertData(quads) {
4813
+ if (this.options.graphType === "qlever") {
4814
+ return this._db.insertData(quads);
4815
+ }
4816
+ return Promise.reject(new Error("insertData not supported for Fuseki \u2014 use update() with SPARQL INSERT DATA"));
4817
+ }
4818
+ /**
4819
+ * Delete quads using the backend's preferred bulk-delete mechanism.
4820
+ * For QLever: uses the /data/delete endpoint.
4821
+ * For Fuseki: falls back to SPARQL DELETE DATA.
4822
+ */
4823
+ deleteData(quads) {
4824
+ if (this.options.graphType === "qlever") {
4825
+ return this._db.deleteData(quads);
4826
+ }
4827
+ return Promise.reject(new Error("deleteData not supported for Fuseki \u2014 use update() with SPARQL DELETE DATA"));
4828
+ }
4721
4829
  };
4722
4830
 
4723
4831
  // libs/js/databases/src/lib/blob/blob.ts
@@ -4778,6 +4886,7 @@ var CueBlobStorage = class {
4778
4886
  var import_promises3 = require("fs/promises");
4779
4887
  var import_path3 = require("path");
4780
4888
  var import_url = require("url");
4889
+ var import_os = require("os");
4781
4890
 
4782
4891
  // libs/js/models/src/lib/file-extensions.ts
4783
4892
  var fileExtensionsInfo = {
@@ -5678,106 +5787,20 @@ var import_uuid4 = require("uuid");
5678
5787
  var import_uuid5 = require("uuid");
5679
5788
 
5680
5789
  // libs/js/rdf-document-writers/src/lib/alternative-representation.ts
5681
- var import_n32 = require("n3");
5790
+ var import_n33 = require("n3");
5682
5791
 
5683
5792
  // libs/js/rdf-document-writers/src/lib/file-location.ts
5684
- var import_n3 = require("n3");
5685
- var { namedNode, literal } = import_n3.DataFactory;
5793
+ var import_n32 = require("n3");
5794
+ var { namedNode, literal } = import_n32.DataFactory;
5686
5795
  var a = namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
5687
5796
  var prefixes = {
5688
5797
  qcy: qaecyPrefixes["qcy"],
5689
5798
  "qcy-e": qaecyPrefixes["qcy-e"],
5690
5799
  xsd: prefixCC["xsd"]
5691
5800
  };
5692
- function fileLocationRaw(metadata, size, fileContentClasses = ["qcy:FileContent"], writer = new import_n3.Writer({ prefixes }), remoteProviderType = "GCP" /* GCP */) {
5693
- if (metadata.md5_hash === void 0)
5694
- throw new Error("md5_hash must be specified");
5695
- if (metadata.blob_name === void 0)
5696
- throw new Error("blob_name must be specified");
5697
- if (metadata.suffix === void 0)
5698
- throw new Error("suffix must be specified");
5699
- if (metadata.name === void 0)
5700
- throw new Error("name must be specified");
5701
- const ids = extractIdsFromRawPath(metadata.blob_name);
5702
- const fileInfo = getFileInfo(metadata.suffix);
5703
- const mimeCategory = fileTypeToMimeCategory[fileInfo.type];
5704
- const originalName = metadata.name.split("/").pop() ?? metadata.name;
5705
- const fileContent = namedNode(ids.documentUUID);
5706
- const fileLocationUUID = generateFileUUID(
5707
- metadata.name,
5708
- metadata.provider_id ?? ""
5709
- );
5710
- const fileLocation = namedNode(fileLocationUUID);
5711
- fileContentClasses.forEach((cl) => {
5712
- writer.addQuad(fileContent, a, namedNode(cl));
5713
- });
5714
- writer.addQuad(
5715
- fileContent,
5716
- namedNode("qcy:hasMimeCategory"),
5717
- namedNode(`qcy-e:${mimeCategory}`)
5718
- );
5719
- writer.addQuad(
5720
- fileContent,
5721
- namedNode("qcy:md5Hash"),
5722
- literal(metadata.md5_hash)
5723
- );
5724
- writer.addQuad(fileContent, namedNode("qcy:mime"), literal(fileInfo.mime));
5725
- writer.addQuad(
5726
- fileContent,
5727
- namedNode("qcy:openFormat"),
5728
- literal(fileInfo?.open)
5729
- );
5730
- writer.addQuad(fileContent, namedNode("qcy:sizeBytes"), literal(size));
5731
- writer.addQuad(fileContent, namedNode("qcy:hasFileLocation"), fileLocation);
5732
- writer.addQuad(fileLocation, a, namedNode("qcy:FileLocation"));
5733
- writer.addQuad(
5734
- fileLocation,
5735
- namedNode("qcy:filePath"),
5736
- literal(metadata.name)
5737
- );
5738
- writer.addQuad(
5739
- fileLocation,
5740
- namedNode("qcy:remoteProviderType"),
5741
- namedNode(`qcy-e:${remoteProviderType}`)
5742
- );
5743
- writer.addQuad(
5744
- fileLocation,
5745
- namedNode("qcy:remoteProviderId"),
5746
- literal(metadata.provider_id ?? "")
5747
- );
5748
- writer.addQuad(
5749
- fileLocation,
5750
- namedNode("qcy:remoteRelativePath"),
5751
- literal(metadata.blob_name)
5752
- );
5753
- writer.addQuad(
5754
- fileLocation,
5755
- namedNode("qcy:suffix"),
5756
- literal(metadata.suffix)
5757
- );
5758
- writer.addQuad(
5759
- fileLocation,
5760
- namedNode("qcy:dateCreated"),
5761
- literal((/* @__PURE__ */ new Date()).toISOString(), namedNode("xsd:dateTime"))
5762
- );
5763
- writer.addQuad(fileLocation, namedNode("qcy:value"), literal(originalName));
5764
- return writer;
5765
- }
5766
- function getFileInfo(suffix) {
5767
- let fileInfo = fileExtensionsInfo[suffix];
5768
- if (fileInfo === void 0) {
5769
- fileInfo = {
5770
- type: "unknown" /* UNKNOWN */,
5771
- open: false,
5772
- suffix,
5773
- mime: "application/octet-stream"
5774
- };
5775
- }
5776
- return fileInfo;
5777
- }
5778
5801
 
5779
5802
  // libs/js/rdf-document-writers/src/lib/alternative-representation.ts
5780
- var { namedNode: namedNode2 } = import_n32.DataFactory;
5803
+ var { namedNode: namedNode2 } = import_n33.DataFactory;
5781
5804
  var prefixes2 = {
5782
5805
  qcy: qaecyPrefixes["qcy"],
5783
5806
  "qcy-e": qaecyPrefixes["qcy-e"],
@@ -5785,12 +5808,12 @@ var prefixes2 = {
5785
5808
  };
5786
5809
 
5787
5810
  // libs/js/rdf-document-writers/src/lib/custom-template-parser.ts
5788
- var import_n33 = require("n3");
5789
- var { namedNode: namedNode3, literal: literal2 } = import_n33.DataFactory;
5811
+ var import_n34 = require("n3");
5812
+ var { namedNode: namedNode3, literal: literal2 } = import_n34.DataFactory;
5790
5813
  var a2 = namedNode3("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
5791
5814
 
5792
5815
  // libs/js/rdf-document-writers/src/lib/document-file.ts
5793
- var import_n34 = require("n3");
5816
+ var import_n35 = require("n3");
5794
5817
 
5795
5818
  // libs/js/rdf-document-writers/src/lib/file-suffix.ts
5796
5819
  function getFileSuffix(filename) {
@@ -5802,28 +5825,13 @@ function getFileSuffix(filename) {
5802
5825
  }
5803
5826
 
5804
5827
  // libs/js/rdf-document-writers/src/lib/document-file.ts
5805
- var { namedNode: namedNode4, literal: literal3 } = import_n34.DataFactory;
5828
+ var { namedNode: namedNode4, literal: literal3 } = import_n35.DataFactory;
5806
5829
  var a3 = namedNode4("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
5807
5830
 
5808
5831
  // libs/js/rdf-document-writers/src/lib/process-logs.ts
5809
- var import_n35 = require("n3");
5810
-
5811
- // libs/js/rdf-document-writers/src/lib/serialize-rdf.ts
5812
- async function serializeRDF(namespace, writer, format = "ttl") {
5813
- return new Promise((resolve2, reject) => {
5814
- writer.end((err, res) => {
5815
- if (err)
5816
- reject(err);
5817
- const addBase = format === "ttl" && namespace !== "";
5818
- resolve2(addBase ? `@base <${namespace}>.
5819
- ${res}` : res);
5820
- });
5821
- });
5822
- }
5823
-
5824
- // libs/js/rdf-document-writers/src/lib/process-logs.ts
5832
+ var import_n36 = require("n3");
5825
5833
  var import_uuid6 = require("uuid");
5826
- var { namedNode: namedNode5, literal: literal4 } = import_n35.DataFactory;
5834
+ var { namedNode: namedNode5, literal: literal4 } = import_n36.DataFactory;
5827
5835
  var a4 = namedNode5("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
5828
5836
  var prefixes3 = {
5829
5837
  qcy: qaecyPrefixes["qcy"],
@@ -5861,20 +5869,6 @@ function uploadedFileMetadata(originalName, projectId, userId, md5, providerId,
5861
5869
  })
5862
5870
  };
5863
5871
  }
5864
- function turtleFileMetadata(sourceFile, processorId, locationUUID, stored = false) {
5865
- const ids = extractIdsFromRawPath(sourceFile);
5866
- const blob_name = locationUUID !== void 0 ? `${ids.projectId}/triples/${ids.documentUUID}_${locationUUID}_${processorId}.ttl` : `${ids.projectId}/triples/${ids.documentUUID}_${processorId}.ttl`;
5867
- const identifier = locationUUID !== void 0 ? `${ids.documentUUID}_${locationUUID}` : ids.documentUUID;
5868
- return {
5869
- blob_name,
5870
- processor: processorId,
5871
- space_id: ids.projectId,
5872
- stored: stored ? "True" : "False",
5873
- suffix: ".ttl",
5874
- identifier,
5875
- source: sourceFile
5876
- };
5877
- }
5878
5872
 
5879
5873
  // libs/js/cue-sdk/src/lib/sync.ts
5880
5874
  var _scanFn = null;
@@ -5891,22 +5885,89 @@ async function _initWasm() {
5891
5885
  _scanFn = mod.scan;
5892
5886
  }
5893
5887
  var DEFAULT_GRAPH_TYPE = "fuseki";
5894
- var DEFAULT_SERVICE_ID = "cue-cli";
5895
- var DEFAULT_RDF_BASE = "https://cue.qaecy.com/r/";
5896
5888
  var ENDPOINT_FUSEKI_QUERY = "/triplestore/query";
5897
5889
  var ENDPOINT_QLEVER_QUERY = "/sparql/query";
5898
5890
  var ENDPOINT_FUSEKI_UPDATE = "/triplestore/update";
5899
5891
  var ENDPOINT_QLEVER_UPDATE = "/sparql/update";
5892
+ var ENDPOINT_FSS_BATCH = "/commands/file-system-structure/batch";
5893
+ var FSS_BATCH_CHUNK_SIZE = 1e3;
5894
+ var PENDING_LS_PREFIX = "cue:pending:";
5895
+ function _pendingFilePath(spaceId) {
5896
+ return (0, import_path3.join)((0, import_os.tmpdir)(), `cue-sync-pending-${spaceId}.json`);
5897
+ }
5898
+ async function _loadPending(spaceId) {
5899
+ if (typeof window !== "undefined") {
5900
+ const raw = window.localStorage.getItem(`${PENDING_LS_PREFIX}${spaceId}`);
5901
+ return raw ? JSON.parse(raw) : null;
5902
+ }
5903
+ try {
5904
+ const raw = await (0, import_promises3.readFile)(_pendingFilePath(spaceId), "utf-8");
5905
+ return JSON.parse(raw);
5906
+ } catch {
5907
+ return null;
5908
+ }
5909
+ }
5910
+ async function _savePending(batch) {
5911
+ const data = JSON.stringify(batch);
5912
+ if (typeof window !== "undefined") {
5913
+ window.localStorage.setItem(`${PENDING_LS_PREFIX}${batch.spaceId}`, data);
5914
+ return;
5915
+ }
5916
+ await (0, import_promises3.writeFile)(_pendingFilePath(batch.spaceId), data, "utf-8");
5917
+ }
5918
+ async function _clearPending(spaceId) {
5919
+ if (typeof window !== "undefined") {
5920
+ window.localStorage.removeItem(`${PENDING_LS_PREFIX}${spaceId}`);
5921
+ return;
5922
+ }
5923
+ try {
5924
+ await (0, import_promises3.unlink)(_pendingFilePath(spaceId));
5925
+ } catch {
5926
+ }
5927
+ }
5900
5928
  var CueSyncApi = class {
5901
- constructor(_auth, _projects, _blob, _gatewayUrl, _serviceId = DEFAULT_SERVICE_ID, _rdfBase = DEFAULT_RDF_BASE) {
5929
+ constructor(_auth, _projects, _blob, _gatewayUrl) {
5902
5930
  this._auth = _auth;
5903
5931
  this._projects = _projects;
5904
5932
  this._blob = _blob;
5905
5933
  this._gatewayUrl = _gatewayUrl;
5906
- this._serviceId = _serviceId;
5907
- this._rdfBase = _rdfBase;
5908
5934
  }
5909
5935
  _graphMap = /* @__PURE__ */ new Map();
5936
+ _api;
5937
+ _pendingItems = [];
5938
+ _pendingSpaceId = null;
5939
+ _flushTimer = null;
5940
+ /** @internal Injected by CueApi after construction to avoid circular dependency. */
5941
+ _bindApi(api) {
5942
+ this._api = api;
5943
+ }
5944
+ /**
5945
+ * Returns a preview of what would be synced: cost breakdown for new files only,
5946
+ * units required, and units still available. Use this before calling {@link sync}
5947
+ * to show the user an accurate cost estimate.
5948
+ */
5949
+ async previewSync(localFiles, options) {
5950
+ const { spaceId, providerId, verbose } = options;
5951
+ const token = await this._auth.getToken();
5952
+ if (!token)
5953
+ throw new Error("Not authenticated. Call cue.auth.signIn() first.");
5954
+ const graph = await this._getOrCreateGraph(spaceId, token);
5955
+ const [remoteFiles, consumption] = await Promise.all([
5956
+ this._listRemoteFiles(graph, spaceId, providerId, verbose),
5957
+ this._api?.getConsumption(spaceId) ?? Promise.reject(new Error("CueSyncApi is not bound to a CueApi instance"))
5958
+ ]);
5959
+ const report = await compareLocalRemote(localFiles, remoteFiles);
5960
+ const toUpload = report.localNotOnRemote ?? [];
5961
+ const costRecords = toUpload.length > 0 ? await this.scanCost(toUpload) : [];
5962
+ const unitsToConsume = costRecords.reduce((sum, r) => sum + r.units, 0);
5963
+ return {
5964
+ costRecords,
5965
+ unitsToConsume,
5966
+ unitsAvailable: consumption.unitsAvailable,
5967
+ filesToUpload: toUpload.length,
5968
+ totalLocalFiles: localFiles.length
5969
+ };
5970
+ }
5910
5971
  async sync(localFiles, options) {
5911
5972
  const { spaceId, providerId, userId, verbose } = options;
5912
5973
  const token = await this._auth.getToken();
@@ -5915,7 +5976,11 @@ var CueSyncApi = class {
5915
5976
  const graph = await this._getOrCreateGraph(spaceId, token);
5916
5977
  if (verbose)
5917
5978
  console.info("Listing remote files \u23F3");
5918
- const remoteFiles = await this._listRemoteFiles(graph, spaceId, providerId, verbose);
5979
+ const [remoteFiles, consumption] = await Promise.all([
5980
+ this._listRemoteFiles(graph, spaceId, providerId, verbose),
5981
+ this._api?.getConsumption(spaceId) ?? Promise.reject(new Error("CueSyncApi is not bound to a CueApi instance"))
5982
+ ]);
5983
+ const { unitsAvailable } = consumption;
5919
5984
  const report = await compareLocalRemote(localFiles, remoteFiles);
5920
5985
  if (verbose) {
5921
5986
  console.info(`Total local files: ${localFiles.length}`);
@@ -5929,6 +5994,16 @@ var CueSyncApi = class {
5929
5994
  let failedUploads = 0;
5930
5995
  let rdfWritten = false;
5931
5996
  const toUpload = report.localNotOnRemote ?? [];
5997
+ if (toUpload.length > 0) {
5998
+ const costRecords = await this.scanCost(toUpload);
5999
+ const unitsToConsume = costRecords.reduce((sum, r) => sum + r.units, 0);
6000
+ if (unitsToConsume > unitsAvailable) {
6001
+ throw new Error(
6002
+ `Insufficient units: ${unitsToConsume} units required but only ${unitsAvailable} available.`
6003
+ );
6004
+ }
6005
+ }
6006
+ await this._initPendingBatch(spaceId, token, verbose);
5932
6007
  if (verbose && toUpload.length)
5933
6008
  console.info("Syncing missing files \u23F3");
5934
6009
  for (const file of toUpload) {
@@ -5948,9 +6023,14 @@ var CueSyncApi = class {
5948
6023
  new Uint8Array(fileBuffer),
5949
6024
  rawMeta
5950
6025
  );
5951
- const uploaded = await this._uploadRdfMetadata(file, rawMeta, verbose);
5952
- if (uploaded)
5953
- rdfWritten = true;
6026
+ await this._queueFileLocation({
6027
+ relativePath: file.relativePath,
6028
+ md5: file.md5,
6029
+ size: file.size,
6030
+ providerId,
6031
+ fileContentExists: false
6032
+ });
6033
+ rdfWritten = true;
5954
6034
  syncCount += 1;
5955
6035
  syncSize += file.size || 0;
5956
6036
  this._logProgress(syncCount, report.totalCount, syncSize, report.totalSize, verbose);
@@ -5964,22 +6044,20 @@ var CueSyncApi = class {
5964
6044
  if (verbose && report.localNotOnRemotePathOnly.length)
5965
6045
  console.info(`Syncing missing file locations (on provider "${providerId}") \u23F3`);
5966
6046
  for (const file of report.localNotOnRemotePathOnly) {
5967
- const rawMeta = uploadedFileMetadata(
5968
- file.relativePath,
5969
- spaceId,
5970
- userId,
5971
- file.md5,
5972
- providerId
5973
- );
5974
- if (!rawMeta.blob_name)
5975
- throw new Error(`blob_name missing for ${file.relativePath}`);
5976
- const uploaded = await this._uploadRdfMetadata(file, rawMeta, verbose);
5977
- if (uploaded)
5978
- rdfWritten = true;
6047
+ await this._queueFileLocation({
6048
+ relativePath: file.relativePath,
6049
+ md5: file.md5,
6050
+ size: file.size,
6051
+ providerId,
6052
+ fileContentExists: true
6053
+ });
6054
+ rdfWritten = true;
5979
6055
  syncCount += 1;
5980
6056
  syncSize += file.size || 0;
5981
6057
  this._logProgress(syncCount, report.totalCount, syncSize, report.totalSize, verbose);
5982
6058
  }
6059
+ await this._drainPending(verbose);
6060
+ this._stopFlushTimer();
5983
6061
  return {
5984
6062
  syncCount,
5985
6063
  syncSize,
@@ -6065,30 +6143,77 @@ WHERE {
6065
6143
  }
6066
6144
  return map;
6067
6145
  }
6068
- async _uploadRdfMetadata(file, rawMeta, verbose) {
6069
- if (!rawMeta.blob_name)
6070
- throw new Error(`blob_name missing for ${file.relativePath}`);
6071
- const ttlMeta = turtleFileMetadata(
6072
- rawMeta.blob_name,
6073
- this._serviceId,
6074
- file.locationUUID
6075
- );
6076
- if (!ttlMeta.blob_name)
6077
- throw new Error(`ttl blob_name missing for ${rawMeta.blob_name}`);
6078
- const writer = fileLocationRaw(rawMeta, file.size);
6079
- const namespace = `${this._rdfBase}${ttlMeta.space_id}/`;
6080
- const triples = await serializeRDF(namespace, writer);
6081
- const uploaded = await this._blob.uploadProcessed(
6082
- ttlMeta.blob_name,
6083
- new Uint8Array(Buffer.from(triples, "utf-8")),
6084
- ttlMeta
6085
- );
6086
- if (!uploaded && verbose) {
6087
- console.info(
6088
- `Graph data for ${file.relativePath} already exists \u2014 skipping \u26A0\uFE0F`
6146
+ async _initPendingBatch(spaceId, token, verbose) {
6147
+ this._pendingSpaceId = spaceId;
6148
+ this._pendingItems = [];
6149
+ const existing = await _loadPending(spaceId);
6150
+ if (existing && existing.items.length > 0) {
6151
+ if (verbose)
6152
+ console.info(`Flushing ${existing.items.length} pending file location(s) from previous sync \u23F3`);
6153
+ await this._flushBatch(existing.items, spaceId, token, verbose);
6154
+ }
6155
+ const timer = setInterval(() => {
6156
+ this._drainPending(verbose).catch(
6157
+ (err) => console.error("[CueSyncApi] Periodic flush failed:", err)
6089
6158
  );
6159
+ }, 6e4);
6160
+ if (typeof timer === "object" && typeof timer.unref === "function") {
6161
+ timer.unref();
6162
+ }
6163
+ this._flushTimer = timer;
6164
+ }
6165
+ async _queueFileLocation(item) {
6166
+ this._pendingItems.push(item);
6167
+ if (this._pendingSpaceId) {
6168
+ await _savePending({ spaceId: this._pendingSpaceId, items: this._pendingItems });
6169
+ }
6170
+ }
6171
+ async _drainPending(verbose) {
6172
+ if (!this._pendingSpaceId || this._pendingItems.length === 0)
6173
+ return;
6174
+ const token = await this._auth.getToken();
6175
+ if (!token)
6176
+ return;
6177
+ await this._flushBatch(this._pendingItems, this._pendingSpaceId, token, verbose);
6178
+ }
6179
+ async _flushBatch(items, spaceId, token, verbose) {
6180
+ const snapshot = [...items];
6181
+ if (this._pendingSpaceId === spaceId)
6182
+ this._pendingItems = [];
6183
+ await _clearPending(spaceId);
6184
+ try {
6185
+ for (let i = 0; i < snapshot.length; i += FSS_BATCH_CHUNK_SIZE) {
6186
+ await this._postFssBatch(snapshot.slice(i, i + FSS_BATCH_CHUNK_SIZE), spaceId, token);
6187
+ }
6188
+ if (verbose)
6189
+ console.info(`Wrote ${snapshot.length} file location(s) to commands API \u2705`);
6190
+ } catch (err) {
6191
+ const restored = [...snapshot, ...this._pendingItems];
6192
+ this._pendingItems = restored;
6193
+ await _savePending({ spaceId, items: restored });
6194
+ throw err;
6195
+ }
6196
+ }
6197
+ async _postFssBatch(items, spaceId, token) {
6198
+ const response = await fetch(`${this._gatewayUrl}${ENDPOINT_FSS_BATCH}`, {
6199
+ method: "POST",
6200
+ headers: {
6201
+ Authorization: `Bearer ${token}`,
6202
+ "Content-Type": "application/json",
6203
+ "x-project-id": spaceId
6204
+ },
6205
+ body: JSON.stringify({ items })
6206
+ });
6207
+ if (!response.ok) {
6208
+ const body = await response.text().catch(() => "");
6209
+ throw new Error(`File structure batch POST failed: ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}`);
6210
+ }
6211
+ }
6212
+ _stopFlushTimer() {
6213
+ if (this._flushTimer !== null) {
6214
+ clearInterval(this._flushTimer);
6215
+ this._flushTimer = null;
6090
6216
  }
6091
- return uploaded;
6092
6217
  }
6093
6218
  /**
6094
6219
  * Scans `localFiles` and returns a per-extension cost breakdown.
@@ -6101,15 +6226,31 @@ WHERE {
6101
6226
  if (!_wasmInitPromise)
6102
6227
  _wasmInitPromise = _initWasm();
6103
6228
  await _wasmInitPromise;
6104
- const entries = await Promise.all(
6105
- localFiles.map(async (f) => ({
6106
- originalPath: f.relativePath,
6107
- data: new Uint8Array(await (0, import_promises3.readFile)(f.fullPath))
6108
- }))
6109
- );
6110
6229
  if (!_scanFn)
6111
6230
  throw new Error("WASM scan function not initialised");
6112
- return _scanFn(entries);
6231
+ const BATCH_SIZE = 200;
6232
+ const merged = /* @__PURE__ */ new Map();
6233
+ for (let i = 0; i < localFiles.length; i += BATCH_SIZE) {
6234
+ const batch = localFiles.slice(i, i + BATCH_SIZE);
6235
+ const entries = await Promise.all(
6236
+ batch.map(async (f) => ({
6237
+ originalPath: f.relativePath,
6238
+ data: new Uint8Array(await (0, import_promises3.readFile)(f.fullPath))
6239
+ }))
6240
+ );
6241
+ const records = _scanFn(entries);
6242
+ for (const r of records) {
6243
+ const existing = merged.get(r.ext);
6244
+ if (existing) {
6245
+ existing.count += r.count;
6246
+ existing.units += r.units;
6247
+ existing.sizeMb += r.sizeMb;
6248
+ } else {
6249
+ merged.set(r.ext, { ...r });
6250
+ }
6251
+ }
6252
+ }
6253
+ return Array.from(merged.values());
6113
6254
  }
6114
6255
  _logProgress(syncCount, totalCount, syncSize, totalSize, verbose) {
6115
6256
  if (!verbose || totalCount === 0)
@@ -6141,7 +6282,9 @@ var CueNode = class extends Cue {
6141
6282
  }
6142
6283
  const blob = new CueBlobStorage({ storageRaw, storageProcessed });
6143
6284
  const syncApi = new CueSyncApi(this.auth, projects, blob, this._endpoints.gatewayUrl);
6144
- return new CueApi(this.auth, this._endpoints.gatewayUrl, projects, syncApi);
6285
+ const api = new CueApi(this.auth, this._endpoints.gatewayUrl, projects, syncApi);
6286
+ syncApi._bindApi(api);
6287
+ return api;
6145
6288
  }
6146
6289
  };
6147
6290
 
@@ -6324,7 +6467,7 @@ async function dumpProcessedHandler(options) {
6324
6467
  var import_fs2 = require("fs");
6325
6468
  var import_stream = require("stream");
6326
6469
  var import_fs3 = require("fs");
6327
- var import_zlib = require("zlib");
6470
+ var import_zlib2 = require("zlib");
6328
6471
  var import_promises5 = require("stream/promises");
6329
6472
  var import_promises6 = require("fs/promises");
6330
6473
  var import_auth9 = require("firebase/auth");
@@ -6504,7 +6647,7 @@ async function retryWithBackoff(fn, maxRetries, delayMs, label) {
6504
6647
  async function _doGzip(filePath) {
6505
6648
  const gzFilePath = `${filePath}.gz`;
6506
6649
  await new Promise((resolve2, reject) => {
6507
- const gzip = (0, import_zlib.createGzip)();
6650
+ const gzip = (0, import_zlib2.createGzip)();
6508
6651
  const source = (0, import_fs3.createReadStream)(filePath);
6509
6652
  const dest = (0, import_fs2.createWriteStream)(gzFilePath);
6510
6653
  (0, import_promises5.pipeline)(source, gzip, dest).then(resolve2).catch(reject);
@@ -6860,24 +7003,46 @@ async function syncHandler(options) {
6860
7003
  }
6861
7004
  if (verbose)
6862
7005
  console.info("Built sync base \u2705\n");
6863
- const costRecords = await cue.api.sync.scanCost(localFiles);
6864
- const totalUnits = costRecords.reduce((sum, r) => sum + r.units, 0);
6865
- console.info("Cost estimate:");
6866
- for (const r of costRecords) {
6867
- console.info(` .${r.ext}: ${r.count} file(s) \u2192 ${r.units} unit(s)`);
7006
+ if (verbose)
7007
+ console.info("Authenticating \u23F3");
7008
+ const user = await cue.auth.signInWithApiKey(key, space);
7009
+ if (verbose)
7010
+ console.info("Authenticated \u2705\n");
7011
+ if (verbose)
7012
+ console.info("Checking sync preview \u23F3");
7013
+ const preview = await cue.api.sync.previewSync(localFiles, {
7014
+ spaceId: space,
7015
+ providerId: provider,
7016
+ userId: user.uid,
7017
+ verbose
7018
+ });
7019
+ console.info("Cost estimate (new files only):");
7020
+ if (preview.costRecords.length === 0) {
7021
+ console.info(" Nothing new to sync.");
7022
+ } else {
7023
+ for (const r of preview.costRecords) {
7024
+ console.info(` .${r.ext}: ${r.count} file(s) \u2192 ${r.units} unit(s)`);
7025
+ }
6868
7026
  }
6869
- console.info(` Total: ${totalUnits} unit(s) across ${localFiles.length} file(s)
7027
+ console.info(` New files: ${preview.filesToUpload}/${preview.totalLocalFiles}`);
7028
+ console.info(` Units required: ${preview.unitsToConsume}`);
7029
+ console.info(` Units available: ${preview.unitsAvailable}
6870
7030
  `);
7031
+ if (preview.filesToUpload === 0) {
7032
+ console.info("Everything is already synced.");
7033
+ process.exit(0);
7034
+ }
7035
+ if (preview.unitsToConsume > preview.unitsAvailable) {
7036
+ console.error(
7037
+ `Insufficient units: ${preview.unitsToConsume} required but only ${preview.unitsAvailable} available.`
7038
+ );
7039
+ process.exit(1);
7040
+ }
6871
7041
  const confirmed = await askConfirm("Continue with sync? (y/n) ");
6872
7042
  if (!confirmed) {
6873
7043
  console.info("Sync cancelled.");
6874
7044
  process.exit(0);
6875
7045
  }
6876
- if (verbose)
6877
- console.info("Authenticating \u23F3");
6878
- const user = await cue.auth.signInWithApiKey(key, space);
6879
- if (verbose)
6880
- console.info("Authenticated \u2705\n");
6881
7046
  const result = await cue.api.sync.sync(localFiles, {
6882
7047
  spaceId: space,
6883
7048
  providerId: provider,
@@ -6923,22 +7088,22 @@ async function syncHandler(options) {
6923
7088
 
6924
7089
  // apps/desktop/cue-cli/src/cue-cli-util-remove-rdf-star.ts
6925
7090
  var import_fs7 = require("fs");
6926
- var import_zlib2 = require("zlib");
7091
+ var import_zlib3 = require("zlib");
6927
7092
 
6928
7093
  // libs/js/rdf-tools/src/lib/nq-to-nt.ts
6929
- var import_n36 = require("n3");
6930
- var { quad, defaultGraph } = import_n36.DataFactory;
7094
+ var import_n37 = require("n3");
7095
+ var { quad, defaultGraph } = import_n37.DataFactory;
6931
7096
 
6932
7097
  // libs/js/rdf-tools/src/lib/remove-rdf-star.ts
6933
- var import_n37 = require("n3");
7098
+ var import_n38 = require("n3");
6934
7099
  var import_stream2 = require("stream");
6935
7100
  function isRDFStarTerm(term) {
6936
7101
  return term.termType === "Quad";
6937
7102
  }
6938
7103
  function removeRDFStar(inputStream, starCount) {
6939
- const parser = new import_n37.Parser({ format: "N-Quads*" });
7104
+ const parser = new import_n38.Parser({ format: "N-Quads*" });
6940
7105
  const outputStream = new import_stream2.PassThrough();
6941
- const writer = new import_n37.StreamWriter({ format: "N-Quads" });
7106
+ const writer = new import_n38.StreamWriter({ format: "N-Quads" });
6942
7107
  writer.pipe(outputStream);
6943
7108
  let count = 0;
6944
7109
  parser.parse(inputStream, (error, quad2, prefixes4) => {
@@ -6971,7 +7136,7 @@ async function utilRemoveRdfStarHandler(options) {
6971
7136
  if (verbose)
6972
7137
  console.info(`Output: ${output}`);
6973
7138
  const fileStream = (0, import_fs7.createReadStream)(input);
6974
- const inputStream = isGzipped ? fileStream.pipe((0, import_zlib2.createGunzip)()) : fileStream;
7139
+ const inputStream = isGzipped ? fileStream.pipe((0, import_zlib3.createGunzip)()) : fileStream;
6975
7140
  let removed = 0;
6976
7141
  const cleanStream = removeRDFStar(inputStream, (count) => {
6977
7142
  removed = count;
@@ -6981,7 +7146,7 @@ async function utilRemoveRdfStarHandler(options) {
6981
7146
  });
6982
7147
  const writeStream = (0, import_fs7.createWriteStream)(output);
6983
7148
  if (isGzipped) {
6984
- const gzip = (0, import_zlib2.createGzip)();
7149
+ const gzip = (0, import_zlib3.createGzip)();
6985
7150
  gzip.pipe(writeStream);
6986
7151
  for await (const chunk of cleanStream) {
6987
7152
  gzip.write(chunk);
@@ -7009,11 +7174,11 @@ async function utilRemoveRdfStarHandler(options) {
7009
7174
  var import_fs8 = require("fs");
7010
7175
 
7011
7176
  // libs/js/rdf-compare/src/lib/js-rdf-compare.ts
7012
- var import_n38 = require("n3");
7177
+ var import_n39 = require("n3");
7013
7178
  var import_jsonld = require("jsonld");
7014
7179
  function parseTurtle(content) {
7015
7180
  return new Promise((resolve2, reject) => {
7016
- const parser = new import_n38.Parser({ format: "Turtle" });
7181
+ const parser = new import_n39.Parser({ format: "Turtle" });
7017
7182
  const quads = [];
7018
7183
  parser.parse(content, (error, quad2) => {
7019
7184
  if (error)
@@ -7027,7 +7192,7 @@ function parseTurtle(content) {
7027
7192
  }
7028
7193
  function quadsToNQuads(quads) {
7029
7194
  return new Promise((resolve2, reject) => {
7030
- const writer = new import_n38.Writer({ format: "N-Quads" });
7195
+ const writer = new import_n39.Writer({ format: "N-Quads" });
7031
7196
  writer.addQuads(quads);
7032
7197
  writer.end((error, result) => {
7033
7198
  if (error)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qaecy/cue-cli",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "description": "Cue CLI for QAECY platform",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -21,7 +21,6 @@
21
21
  "jsonld": "8.3.3",
22
22
  "jszip": "3.10.1",
23
23
  "n3": "1.26.0",
24
- "nanoid": "5.1.6",
25
24
  "oxigraph": "0.5.3",
26
25
  "protobufjs": "7.5.4",
27
26
  "spark-md5": "3.0.2",