@qaecy/cue-sdk 0.0.4 → 0.0.5

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.
@@ -1,9 +1,8 @@
1
1
  import { initializeApp as D } from "firebase/app";
2
- import { getAuth as S, connectAuthEmulator as C, signInWithEmailAndPassword as v, GoogleAuthProvider as P, OAuthProvider as U, signInWithPopup as T, signInWithCustomToken as O, signOut as j, onAuthStateChanged as K, fetchSignInMethodsForEmail as N, linkWithPopup as R, unlink as q, reauthenticateWithCredential as y, EmailAuthProvider as m, updatePassword as $, linkWithCredential as H, verifyBeforeUpdateEmail as x, sendEmailVerification as L } from "firebase/auth";
3
- import { getFirestore as b, connectFirestoreEmulator as A, doc as _, collection as o, getDoc as f, setDoc as E, getDocs as u, query as h, where as l, serverTimestamp as W, increment as F, addDoc as z, deleteDoc as B, limit as J } from "firebase/firestore";
4
- import { nanoid as M } from "nanoid";
5
- const G = "microsoft.com";
6
- class Q {
2
+ import { getAuth as S, connectAuthEmulator as C, signInWithEmailAndPassword as T, GoogleAuthProvider as P, OAuthProvider as U, signInWithPopup as O, signInWithCustomToken as j, signOut as v, onAuthStateChanged as N, fetchSignInMethodsForEmail as K, linkWithPopup as R, unlink as $, reauthenticateWithCredential as f, EmailAuthProvider as m, updatePassword as q, linkWithCredential as H, verifyBeforeUpdateEmail as x, sendEmailVerification as L } from "firebase/auth";
3
+ import { getFirestore as b, connectFirestoreEmulator as A, doc as _, collection as n, getDoc as y, setDoc as g, getDocs as h, query as l, where as d, serverTimestamp as F, increment as W, addDoc as z, deleteDoc as B, limit as J } from "firebase/firestore";
4
+ const M = "microsoft.com";
5
+ class V {
7
6
  _auth;
8
7
  _endpoints;
9
8
  constructor(t, e = !1, r) {
@@ -15,14 +14,14 @@ class Q {
15
14
  if (t === "password") {
16
15
  if (!e)
17
16
  throw new Error("credentials are required for password sign-in");
18
- return (await v(
17
+ return (await T(
19
18
  this._auth,
20
19
  e.email,
21
20
  e.password
22
21
  )).user;
23
22
  }
24
- const r = t === "google" ? new P() : new U(G);
25
- return (await T(this._auth, r)).user;
23
+ const r = t === "google" ? new P() : new U(M);
24
+ return (await O(this._auth, r)).user;
26
25
  }
27
26
  /** Sign in with a Cue API key */
28
27
  async signInWithApiKey(t, e) {
@@ -36,11 +35,11 @@ class Q {
36
35
  if (!r.ok)
37
36
  throw new Error(`Failed to fetch custom token: ${r.statusText}`);
38
37
  const { token: s } = await r.json();
39
- return (await O(this._auth, s)).user;
38
+ return (await j(this._auth, s)).user;
40
39
  }
41
40
  /** Sign out the current user */
42
41
  async signOut() {
43
- await j(this._auth);
42
+ await v(this._auth);
44
43
  }
45
44
  /** Currently signed-in user, or null if not authenticated */
46
45
  get currentUser() {
@@ -48,7 +47,7 @@ class Q {
48
47
  }
49
48
  /** Subscribe to authentication state changes. Returns an unsubscribe function. */
50
49
  onAuthStateChanged(t) {
51
- return K(this._auth, t);
50
+ return N(this._auth, t);
52
51
  }
53
52
  /** Get the Firebase ID token for the current user, or null if not authenticated */
54
53
  async getToken(t = !1) {
@@ -60,7 +59,7 @@ class Q {
60
59
  return this._auth;
61
60
  }
62
61
  }
63
- const V = "/assistant/search", Y = "/triplestore/query";
62
+ const G = "/assistant/search", Q = "/triplestore/query", Y = "/data-views/admin/consumption/units";
64
63
  class X {
65
64
  constructor(t, e, r, s) {
66
65
  this._auth = t, this._gatewayUrl = e, this.projects = r, this.sync = s;
@@ -89,7 +88,7 @@ class X {
89
88
  * The user must be authenticated before calling this.
90
89
  */
91
90
  async search(t) {
92
- const e = await this._authHeaders(), r = await fetch(`${this._gatewayUrl}${V}`, {
91
+ const e = await this._authHeaders(), r = await fetch(`${this._gatewayUrl}${G}`, {
93
92
  method: "POST",
94
93
  headers: e,
95
94
  body: JSON.stringify({
@@ -107,7 +106,7 @@ class X {
107
106
  * The user must be authenticated before calling this.
108
107
  */
109
108
  async sparql(t, e) {
110
- const r = await this._authHeaders(), s = await fetch(`${this._gatewayUrl}${Y}`, {
109
+ const r = await this._authHeaders(), s = await fetch(`${this._gatewayUrl}${Q}`, {
111
110
  method: "POST",
112
111
  headers: r,
113
112
  body: JSON.stringify({ query: t, projectId: e })
@@ -116,6 +115,14 @@ class X {
116
115
  throw new Error(`SPARQL query failed: ${s.status} ${s.statusText}`);
117
116
  return s.json();
118
117
  }
118
+ async getConsumption(t) {
119
+ const e = await this._authHeaders(), r = await fetch(`${this._gatewayUrl}${Y}`, {
120
+ headers: { ...e, "x-project-id": t }
121
+ });
122
+ if (!r.ok)
123
+ throw new Error(`Failed to fetch consumption: ${r.status} ${r.statusText}`);
124
+ return r.json();
125
+ }
119
126
  }
120
127
  const w = "projects";
121
128
  class Z {
@@ -137,10 +144,10 @@ class Z {
137
144
  * Throws if a project with the given ID already exists.
138
145
  */
139
146
  async createProject(t) {
140
- const e = this._requireUser(), r = t.id ?? crypto.randomUUID(), s = (/* @__PURE__ */ new Date()).toISOString(), a = { views: [], chatDisabled: !1 }, i = _(o(this._db, w), r);
141
- if ((await f(i)).exists())
147
+ const e = this._requireUser(), r = t.id ?? crypto.randomUUID(), s = (/* @__PURE__ */ new Date()).toISOString(), a = { views: [], chatDisabled: !1 }, i = _(n(this._db, w), r);
148
+ if ((await y(i)).exists())
142
149
  throw new Error(`Project with ID "${r}" already exists.`);
143
- const c = {
150
+ const u = {
144
151
  id: r,
145
152
  name: t.name,
146
153
  organizationID: t.organizationID,
@@ -153,28 +160,28 @@ class Z {
153
160
  alternativeIDs: [r],
154
161
  projectSettings: a
155
162
  };
156
- return await E(i, c), c;
163
+ return await g(i, u), u;
157
164
  }
158
165
  /**
159
166
  * List all projects where the authenticated user appears in the members, syncers, or admins array.
160
167
  * Runs three parallel Firestore queries and deduplicates by project ID.
161
168
  */
162
169
  async listProjects() {
163
- const t = this._requireUser(), e = o(this._db, w), [r, s, a] = await Promise.all([
164
- u(h(e, l("members", "array-contains", t))),
165
- u(h(e, l("syncers", "array-contains", t))),
166
- u(h(e, l("admins", "array-contains", t)))
167
- ]), i = /* @__PURE__ */ new Set(), d = [];
168
- for (const c of [r, s, a])
169
- for (const k of c.docs) {
170
+ const t = this._requireUser(), e = n(this._db, w), [r, s, a] = await Promise.all([
171
+ h(l(e, d("members", "array-contains", t))),
172
+ h(l(e, d("syncers", "array-contains", t))),
173
+ h(l(e, d("admins", "array-contains", t)))
174
+ ]), i = /* @__PURE__ */ new Set(), o = [];
175
+ for (const u of [r, s, a])
176
+ for (const k of u.docs) {
170
177
  const p = k.data();
171
- i.has(p.id) || (i.add(p.id), d.push(p));
178
+ i.has(p.id) || (i.add(p.id), o.push(p));
172
179
  }
173
- return d;
180
+ return o;
174
181
  }
175
182
  /** Fetch a single project by ID. Returns null if not found. */
176
183
  async getProject(t) {
177
- const e = _(o(this._db, w), t), r = await f(e);
184
+ const e = _(n(this._db, w), t), r = await y(e);
178
185
  return r.exists() ? r.data() : null;
179
186
  }
180
187
  /**
@@ -184,14 +191,14 @@ class Z {
184
191
  async incrementUnitsConsumed(t, e, r) {
185
192
  if (e <= 0) return;
186
193
  const s = _(this._db, "clientSync", t);
187
- await E(s, {
188
- unitsConsumed: F(e),
189
- lastUpdated: W(),
194
+ await g(s, {
195
+ unitsConsumed: W(e),
196
+ lastUpdated: F(),
190
197
  lastUserId: r
191
198
  }, { merge: !0 });
192
199
  }
193
200
  }
194
- const g = "apiKeys";
201
+ const E = "apiKeys";
195
202
  class tt {
196
203
  constructor(t, e, r, s, a) {
197
204
  this._auth = t, this._db = b(e), r && A(this._db, s, a);
@@ -208,7 +215,7 @@ class tt {
208
215
  async getSignInMethods() {
209
216
  const t = this._auth.currentUser;
210
217
  if (!t?.email) throw new Error("User has no e-mail");
211
- return N(this._auth.firebaseAuth, t.email);
218
+ return K(this._auth.firebaseAuth, t.email);
212
219
  }
213
220
  /** Builds a human-readable label from a Firebase UserInfo provider entry. */
214
221
  buildProviderLabel(t) {
@@ -228,16 +235,16 @@ class tt {
228
235
  }
229
236
  /** Unlinks a provider from the current account. */
230
237
  async unlinkProvider(t) {
231
- await q(this._requireUser(), t);
238
+ await $(this._requireUser(), t);
232
239
  }
233
240
  /** Changes the password. Reauthenticates first. */
234
241
  async updatePassword(t, e) {
235
242
  const r = this._requireUser();
236
243
  if (!r.email) throw new Error("User has no e-mail");
237
- await y(
244
+ await f(
238
245
  r,
239
246
  m.credential(r.email, t)
240
- ), await $(r, e);
247
+ ), await q(r, e);
241
248
  }
242
249
  /** Adds (sets) a password for an account that currently only uses SSO. */
243
250
  async addPassword(t) {
@@ -249,7 +256,7 @@ class tt {
249
256
  async updateEmail(t, e) {
250
257
  const r = this._requireUser();
251
258
  if (!r.email) throw new Error("User e-mail not available");
252
- await y(
259
+ await f(
253
260
  r,
254
261
  m.credential(r.email, e)
255
262
  ), await x(r, t), await L(r);
@@ -258,8 +265,8 @@ class tt {
258
265
  async createAPIKey(t) {
259
266
  const e = this._auth.currentUser?.uid;
260
267
  if (!e) throw new Error("User not authenticated");
261
- const r = { key: `cue-${M(150)}`, uid: e, expiration: t }, s = o(this._db, g);
262
- return this._apiKeyDocRef = await z(s, r), r;
268
+ const r = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", a = { key: `cue-${Array.from(globalThis.crypto.getRandomValues(new Uint8Array(150)), (o) => r[o & 63]).join("")}`, uid: e, expiration: t }, i = n(this._db, E);
269
+ return this._apiKeyDocRef = await z(i, a), a;
263
270
  }
264
271
  /** Revokes the current user's API key. */
265
272
  async revokeAPIKey() {
@@ -269,11 +276,11 @@ class tt {
269
276
  /** Fetches the current user's existing API key. */
270
277
  async requestAPIKey() {
271
278
  if (!this._apiKeyDocRef) throw new Error("No API key document reference");
272
- const t = (await f(this._apiKeyDocRef)).data();
279
+ const t = (await y(this._apiKeyDocRef)).data();
273
280
  return { key: t.key, expiration: t.expiration };
274
281
  }
275
282
  async _checkIfUserHasAPIKey(t) {
276
- const e = o(this._db, g), r = h(e, l("uid", "==", t), J(1)), s = await u(r);
283
+ const e = n(this._db, E), r = l(e, d("uid", "==", t), J(1)), s = await h(r);
277
284
  return s.empty || (this._apiKeyDocRef = s.docs[0].ref), !s.empty;
278
285
  }
279
286
  _requireUser() {
@@ -302,7 +309,7 @@ const I = "qaecy-mvp-406413", et = "734737865998", rt = {
302
309
  firestoreEmulatorPort: 8080
303
310
  }
304
311
  };
305
- class nt {
312
+ class ot {
306
313
  auth;
307
314
  api;
308
315
  profile;
@@ -318,7 +325,7 @@ class nt {
318
325
  authDomain: `${I}.firebaseapp.com`,
319
326
  projectId: I,
320
327
  messagingSenderId: et
321
- }), this.auth = new Q(this._app, this._isEmulator, this._endpoints);
328
+ }), this.auth = new V(this._app, this._isEmulator, this._endpoints);
322
329
  const r = new Z(this.auth, this._app, this._isEmulator, this._endpoints);
323
330
  this.api = this._buildApi(r), this.profile = new tt(
324
331
  this.auth,
@@ -338,9 +345,9 @@ class nt {
338
345
  }
339
346
  }
340
347
  export {
341
- nt as C,
348
+ ot as C,
342
349
  X as a,
343
- Q as b,
350
+ V as b,
344
351
  tt as c,
345
352
  Z as d
346
353
  };
package/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export { CueAuth } from './lib/auth';
3
3
  export { CueApi } from './lib/api';
4
4
  export { CueProjects } from './lib/project';
5
5
  export { CueProfile } from './lib/profile';
6
- export type { CueSdkConfig, SsoProvider, PasswordCredentials, SearchRequest, SearchResponse, SearchSource, ProjectData, ProjectSettings, CreateProjectOptions, SyncOptions, SyncResult, ScanOutputRecord, ProfileSSOAccount, APIKeyInfo, APIKeyDoc, } from './lib/models';
6
+ export type { CueSdkConfig, SsoProvider, PasswordCredentials, SearchRequest, SearchResponse, SearchSource, ProjectData, ProjectSettings, CreateProjectOptions, SyncOptions, SyncResult, SyncPreview, ScanOutputRecord, UnitsConsumedDto, ProfileSSOAccount, APIKeyInfo, APIKeyDoc, } from './lib/models';
7
7
  export type { AuthStateListener, Unsubscribe } from './lib/auth';
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as s, a as u, b as C, c as o, d as r } from "./cue-Cl66hB4E.js";
1
+ import { C as s, a as u, b as C, c as o, d as r } from "./cue-y-t8nSnt.js";
2
2
  export {
3
3
  s as Cue,
4
4
  u as CueApi,
package/lib/api.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CueAuth } from './auth';
2
- import { SearchRequest, SearchResponse } from './models';
2
+ import { SearchRequest, SearchResponse, UnitsConsumedDto } from './models';
3
3
  import { CueProjects } from './project';
4
4
  import { CueSyncApi } from './sync';
5
5
  export declare class CueApi {
@@ -24,4 +24,5 @@ export declare class CueApi {
24
24
  * The user must be authenticated before calling this.
25
25
  */
26
26
  sparql(query: string, projectId: string): Promise<unknown>;
27
+ getConsumption(projectId: string): Promise<UnitsConsumedDto>;
27
28
  }
package/lib/models.d.ts CHANGED
@@ -110,6 +110,23 @@ export interface ScanOutputRecord {
110
110
  /** Total size of files with this extension in megabytes. */
111
111
  sizeMb: number;
112
112
  }
113
+ export interface UnitsConsumedDto {
114
+ unitsAvailable: number;
115
+ unitsConsumed: number;
116
+ filesProcessed: number;
117
+ }
118
+ export interface SyncPreview {
119
+ /** Per-extension cost breakdown for files not yet synced */
120
+ costRecords: ScanOutputRecord[];
121
+ /** Total units required for the new files */
122
+ unitsToConsume: number;
123
+ /** Units still available for this project */
124
+ unitsAvailable: number;
125
+ /** Number of new files that would be uploaded */
126
+ filesToUpload: number;
127
+ /** Total local files scanned */
128
+ totalLocalFiles: number;
129
+ }
113
130
  export interface SyncResult {
114
131
  /** Number of files successfully synced in this run */
115
132
  syncCount: number;
@@ -123,8 +140,6 @@ export interface SyncResult {
123
140
  totalSize: number;
124
141
  /** Whether any RDF metadata was written */
125
142
  rdfWritten: boolean;
126
- /** Total billable units consumed across all successfully uploaded files in this run */
127
- unitsConsumed: number;
128
143
  }
129
144
  export interface ProfileSSOAccount {
130
145
  id: string;
package/lib/sync.d.ts CHANGED
@@ -1,22 +1,38 @@
1
1
  import { CueBlobStorage } from 'js-databases';
2
2
  import { LocalFile } from 'js-sync-tools';
3
3
  import { CueAuth } from './auth';
4
+ import { CueApi } from './api';
4
5
  import { CueProjects } from './project';
5
- import { ScanOutputRecord, SyncOptions, SyncResult } from './models';
6
+ import { ScanOutputRecord, SyncOptions, SyncPreview, SyncResult } from './models';
6
7
  export declare class CueSyncApi {
7
8
  private readonly _auth;
8
9
  private readonly _projects;
9
10
  private readonly _blob;
10
11
  private readonly _gatewayUrl;
11
- private readonly _serviceId;
12
- private readonly _rdfBase;
13
12
  private readonly _graphMap;
14
- constructor(_auth: CueAuth, _projects: CueProjects, _blob: CueBlobStorage, _gatewayUrl: string, _serviceId?: string, _rdfBase?: string);
13
+ private _api?;
14
+ private _pendingItems;
15
+ private _pendingSpaceId;
16
+ private _flushTimer;
17
+ constructor(_auth: CueAuth, _projects: CueProjects, _blob: CueBlobStorage, _gatewayUrl: string);
18
+ /** @internal Injected by CueApi after construction to avoid circular dependency. */
19
+ _bindApi(api: CueApi): void;
20
+ /**
21
+ * Returns a preview of what would be synced: cost breakdown for new files only,
22
+ * units required, and units still available. Use this before calling {@link sync}
23
+ * to show the user an accurate cost estimate.
24
+ */
25
+ previewSync(localFiles: LocalFile[], options: SyncOptions): Promise<SyncPreview>;
15
26
  sync(localFiles: LocalFile[], options: SyncOptions): Promise<SyncResult>;
16
27
  private _getOrCreateGraph;
17
28
  private _listRemoteFiles;
18
29
  private _getGraphFiles;
19
- private _uploadRdfMetadata;
30
+ private _initPendingBatch;
31
+ private _queueFileLocation;
32
+ private _drainPending;
33
+ private _flushBatch;
34
+ private _postFssBatch;
35
+ private _stopFlushTimer;
20
36
  /**
21
37
  * Scans `localFiles` and returns a per-extension cost breakdown.
22
38
  *
package/node.js CHANGED
@@ -1,161 +1,194 @@
1
- import { C as F, a as D } from "./cue-Cl66hB4E.js";
2
- import { b as ht, c as dt, d as _t } from "./cue-Cl66hB4E.js";
3
- import { getStorage as C, connectStorageEmulator as R } from "firebase/storage";
4
- import { CueGraphDatabase as I, CueBlobStorage as z } from "js-databases";
5
- import { readFile as w } from "fs/promises";
6
- import { resolve as T, join as N, dirname as O } from "path";
7
- import { createRequire as M } from "module";
8
- import { compareLocalRemote as j } from "js-sync-tools";
9
- import { uploadedFileMetadata as S, turtleFileMetadata as B } from "js-file-metadata-helpers";
10
- import { fileLocationRaw as L, serializeRDF as G } from "js/rdf-document-writers";
11
- import { qaecyPrefixes as k } from "js/prefixes";
12
- let q = null, A = null, _ = null;
13
- async function v() {
14
- const t = M(T(process.cwd(), "_anchor.js")).resolve("dir-scanner-wasm"), o = await w(N(O(t), "dir_scanner_wasm_bg.wasm")), e = await import(
15
- /* @vite-ignore */
16
- t
17
- );
18
- await e.default({ module_or_path: o }), q = e.scan, A = e.scan_file_map;
1
+ import { C as v, a as O } from "./cue-y-t8nSnt.js";
2
+ import { b as dt, c as _t, d as gt } from "./cue-y-t8nSnt.js";
3
+ import { getStorage as b, connectStorageEmulator as T } from "firebase/storage";
4
+ import { CueGraphDatabase as q, CueBlobStorage as z } from "js-databases";
5
+ import { readFile as P, writeFile as B, unlink as D } from "fs/promises";
6
+ import { join as y } from "path";
7
+ import { pathToFileURL as j } from "url";
8
+ import { tmpdir as x } from "os";
9
+ import { compareLocalRemote as A } from "js-sync-tools";
10
+ import { uploadedFileMetadata as L } from "js-file-metadata-helpers";
11
+ import { qaecyPrefixes as M } from "js/prefixes";
12
+ let C = null, $ = null;
13
+ async function k() {
14
+ const c = y(__dirname, "assets", "wasm"), t = await P(y(c, "dir_scanner_wasm_bg.wasm")), e = await import(j(y(c, "dir_scanner_wasm.mjs")).href);
15
+ await e.default({ module_or_path: t }), C = e.scan;
16
+ }
17
+ const H = "fuseki", G = "/triplestore/query", W = "/sparql/query", K = "/triplestore/update", J = "/sparql/update", Q = "/commands/file-system-structure/batch", F = 1e3, I = "cue:pending:";
18
+ function U(c) {
19
+ return y(x(), `cue-sync-pending-${c}.json`);
20
+ }
21
+ async function Y(c) {
22
+ if (typeof window < "u") {
23
+ const t = window.localStorage.getItem(`${I}${c}`);
24
+ return t ? JSON.parse(t) : null;
25
+ }
26
+ try {
27
+ const t = await P(U(c), "utf-8");
28
+ return JSON.parse(t);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ async function N(c) {
34
+ const t = JSON.stringify(c);
35
+ if (typeof window < "u") {
36
+ window.localStorage.setItem(`${I}${c.spaceId}`, t);
37
+ return;
38
+ }
39
+ await B(U(c.spaceId), t, "utf-8");
40
+ }
41
+ async function V(c) {
42
+ if (typeof window < "u") {
43
+ window.localStorage.removeItem(`${I}${c}`);
44
+ return;
45
+ }
46
+ try {
47
+ await D(U(c));
48
+ } catch {
49
+ }
19
50
  }
20
- const x = "fuseki", H = "cue-cli", K = "https://cue.qaecy.com/r/", Q = "/triplestore/query", W = "/sparql/query", V = "/triplestore/update", Y = "/sparql/update";
21
51
  class X {
22
- constructor(t, o, e, i, s = H, a = K) {
23
- this._auth = t, this._projects = o, this._blob = e, this._gatewayUrl = i, this._serviceId = s, this._rdfBase = a;
52
+ constructor(t, o, e, i) {
53
+ this._auth = t, this._projects = o, this._blob = e, this._gatewayUrl = i;
24
54
  }
25
55
  _auth;
26
56
  _projects;
27
57
  _blob;
28
58
  _gatewayUrl;
29
- _serviceId;
30
- _rdfBase;
31
59
  _graphMap = /* @__PURE__ */ new Map();
60
+ _api;
61
+ _pendingItems = [];
62
+ _pendingSpaceId = null;
63
+ _flushTimer = null;
64
+ /** @internal Injected by CueApi after construction to avoid circular dependency. */
65
+ _bindApi(t) {
66
+ this._api = t;
67
+ }
68
+ /**
69
+ * Returns a preview of what would be synced: cost breakdown for new files only,
70
+ * units required, and units still available. Use this before calling {@link sync}
71
+ * to show the user an accurate cost estimate.
72
+ */
73
+ async previewSync(t, o) {
74
+ const { spaceId: e, providerId: i, verbose: s } = o, n = await this._auth.getToken();
75
+ if (!n) throw new Error("Not authenticated. Call cue.auth.signIn() first.");
76
+ const r = await this._getOrCreateGraph(e, n), [a, l] = await Promise.all([
77
+ this._listRemoteFiles(r, e, i, s),
78
+ this._api?.getConsumption(e) ?? Promise.reject(new Error("CueSyncApi is not bound to a CueApi instance"))
79
+ ]), p = (await A(t, a)).localNotOnRemote ?? [], h = p.length > 0 ? await this.scanCost(p) : [], _ = h.reduce((g, m) => g + m.units, 0);
80
+ return {
81
+ costRecords: h,
82
+ unitsToConsume: _,
83
+ unitsAvailable: l.unitsAvailable,
84
+ filesToUpload: p.length,
85
+ totalLocalFiles: t.length
86
+ };
87
+ }
32
88
  async sync(t, o) {
33
- const { spaceId: e, providerId: i, userId: s, verbose: a } = o, l = await this._auth.getToken();
34
- if (!l) throw new Error("Not authenticated. Call cue.auth.signIn() first.");
35
- const c = await this._getOrCreateGraph(e, l);
36
- a && console.info("Listing remote files ⏳");
37
- const p = await this._listRemoteFiles(c, e, i, a), n = await j(t, p);
38
- a && (console.info(`Total local files: ${t.length}`), console.info(`Total remote files: ${p.length}`), console.info(
39
- `Total files to sync: ${(n.localNotOnRemote?.length ?? 0) + n.localNotOnRemotePathOnly.length}`
89
+ const { spaceId: e, providerId: i, userId: s, verbose: n } = o, r = await this._auth.getToken();
90
+ if (!r) throw new Error("Not authenticated. Call cue.auth.signIn() first.");
91
+ const a = await this._getOrCreateGraph(e, r);
92
+ n && console.info("Listing remote files ⏳");
93
+ const [l, d] = await Promise.all([
94
+ this._listRemoteFiles(a, e, i, n),
95
+ this._api?.getConsumption(e) ?? Promise.reject(new Error("CueSyncApi is not bound to a CueApi instance"))
96
+ ]), { unitsAvailable: p } = d, h = await A(t, l);
97
+ n && (console.info(`Total local files: ${t.length}`), console.info(`Total remote files: ${l.length}`), console.info(
98
+ `Total files to sync: ${(h.localNotOnRemote?.length ?? 0) + h.localNotOnRemotePathOnly.length}`
40
99
  ));
41
- let u = n.syncCount, m = n.syncSize, U = 0, g = !1, y = 0, P = {};
42
- try {
43
- _ || (_ = v()), await _;
44
- const r = await Promise.all(
45
- t.map(async (h) => ({
46
- originalPath: h.relativePath,
47
- data: new Uint8Array(await w(h.fullPath))
48
- }))
49
- );
50
- P = A(r);
51
- } catch (r) {
52
- console.warn("[CueSyncApi] Unit map failed, proceeding without unit tracking:", r);
100
+ let _ = h.syncCount, g = h.syncSize, m = 0, S = !1;
101
+ const w = h.localNotOnRemote ?? [];
102
+ if (w.length > 0) {
103
+ const f = (await this.scanCost(w)).reduce((E, R) => E + R.units, 0);
104
+ if (f > p)
105
+ throw new Error(
106
+ `Insufficient units: ${f} units required but only ${p} available.`
107
+ );
53
108
  }
54
- const E = n.localNotOnRemote ?? [];
55
- a && E.length && console.info("Syncing missing files ⏳");
56
- for (const r of E) {
57
- let h = !1;
109
+ await this._initPendingBatch(e, r, n), n && w.length && console.info("Syncing missing files ⏳");
110
+ for (const u of w)
58
111
  try {
59
- const f = S(
60
- r.relativePath,
112
+ const f = L(
113
+ u.relativePath,
61
114
  e,
62
115
  s,
63
- r.md5,
116
+ u.md5,
64
117
  i
65
118
  );
66
- if (!f.blob_name) throw new Error(`blob_name missing for ${r.relativePath}`);
67
- const d = await w(r.fullPath);
119
+ if (!f.blob_name) throw new Error(`blob_name missing for ${u.relativePath}`);
120
+ const E = await P(u.fullPath);
68
121
  await this._blob.uploadRaw(
69
122
  f.blob_name,
70
- new Uint8Array(d),
123
+ new Uint8Array(E),
71
124
  f
72
- ), await this._uploadRdfMetadata(r, f, a) && (g = !0), u += 1, m += r.size || 0, h = !0, this._logProgress(u, n.totalCount, m, n.totalSize, a);
125
+ ), await this._queueFileLocation({
126
+ relativePath: u.relativePath,
127
+ md5: u.md5,
128
+ size: u.size,
129
+ providerId: i,
130
+ fileContentExists: !1
131
+ }), S = !0, _ += 1, g += u.size || 0, this._logProgress(_, h.totalCount, g, h.totalSize, n);
73
132
  } catch (f) {
74
- U += 1, console.error(`[CueSyncApi] Failed to upload file: ${r.fullPath}`), a && console.error("[CueSyncApi] Upload error details:", f);
133
+ m += 1, console.error(`[CueSyncApi] Failed to upload file: ${u.fullPath}`), n && console.error("[CueSyncApi] Upload error details:", f);
75
134
  }
76
- if (h) {
77
- const f = P[r.relativePath] ?? 0;
78
- if (f > 0) {
79
- y += f;
80
- try {
81
- await this._projects.incrementUnitsConsumed(e, f, s);
82
- } catch (d) {
83
- console.error(`[CueSyncApi] Failed to write clientSync entry for ${r.relativePath}:`, d);
84
- }
85
- }
86
- }
87
- }
88
- a && n.localNotOnRemotePathOnly.length && console.info(`Syncing missing file locations (on provider "${i}") ⏳`);
89
- for (const r of n.localNotOnRemotePathOnly) {
90
- const h = S(
91
- r.relativePath,
92
- e,
93
- s,
94
- r.md5,
95
- i
96
- );
97
- if (!h.blob_name) throw new Error(`blob_name missing for ${r.relativePath}`);
98
- await this._uploadRdfMetadata(r, h, a) && (g = !0), u += 1, m += r.size || 0;
99
- const d = P[r.relativePath] ?? 0;
100
- if (d > 0) {
101
- y += d;
102
- try {
103
- await this._projects.incrementUnitsConsumed(e, d, s);
104
- } catch ($) {
105
- console.error(`[CueSyncApi] Failed to write clientSync entry for ${r.relativePath}:`, $);
106
- }
107
- }
108
- this._logProgress(u, n.totalCount, m, n.totalSize, a);
109
- }
110
- return {
111
- syncCount: u,
112
- syncSize: m,
113
- failedUploads: U,
114
- totalCount: n.totalCount,
115
- totalSize: n.totalSize,
116
- rdfWritten: g,
117
- unitsConsumed: y
135
+ n && h.localNotOnRemotePathOnly.length && console.info(`Syncing missing file locations (on provider "${i}") ⏳`);
136
+ for (const u of h.localNotOnRemotePathOnly)
137
+ await this._queueFileLocation({
138
+ relativePath: u.relativePath,
139
+ md5: u.md5,
140
+ size: u.size,
141
+ providerId: i,
142
+ fileContentExists: !0
143
+ }), S = !0, _ += 1, g += u.size || 0, this._logProgress(_, h.totalCount, g, h.totalSize, n);
144
+ return await this._drainPending(n), this._stopFlushTimer(), {
145
+ syncCount: _,
146
+ syncSize: g,
147
+ failedUploads: m,
148
+ totalCount: h.totalCount,
149
+ totalSize: h.totalSize,
150
+ rdfWritten: S
118
151
  };
119
152
  }
120
153
  async _getOrCreateGraph(t, o) {
121
154
  const e = this._graphMap.get(t);
122
155
  if (e) return e;
123
- const s = (await this._projects.getProject(t))?.projectSettings?.graph?.type ?? x, a = s === "qlever" ? `${this._gatewayUrl}${W}` : `${this._gatewayUrl}${Q}`, l = s === "qlever" ? `${this._gatewayUrl}${Y}` : `${this._gatewayUrl}${V}`, c = new I({
156
+ const s = (await this._projects.getProject(t))?.projectSettings?.graph?.type ?? H, n = s === "qlever" ? `${this._gatewayUrl}${W}` : `${this._gatewayUrl}${G}`, r = s === "qlever" ? `${this._gatewayUrl}${J}` : `${this._gatewayUrl}${K}`, a = new q({
124
157
  graphType: s,
125
- queryEndpoint: a,
126
- updateEndpoint: l,
158
+ queryEndpoint: n,
159
+ updateEndpoint: r,
127
160
  originalHeaders: {
128
161
  "x-project-id": t,
129
162
  authorization: `Bearer ${o}`
130
163
  }
131
164
  });
132
- return this._graphMap.set(t, c), c;
165
+ return this._graphMap.set(t, a), a;
133
166
  }
134
167
  async _listRemoteFiles(t, o, e, i) {
135
168
  i && console.info(`Listing files in raw space: ${o}`);
136
- const [s, a] = await Promise.all([
169
+ const [s, n] = await Promise.all([
137
170
  this._blob.listRaw(o),
138
171
  this._getGraphFiles(t, e)
139
172
  ]);
140
173
  i && console.info(`Found ${s.length} files in raw store for space: ${o}`);
141
- const l = [];
142
- for (const c of s) {
143
- const p = c.substring(0, 36), n = a[p] ?? [];
144
- if (n.length === 0)
145
- i && console.warn(`No location data found for contentUUID: ${p}`), l.push({ contentUUID: p });
174
+ const r = [];
175
+ for (const a of s) {
176
+ const l = a.substring(0, 36), d = n[l] ?? [];
177
+ if (d.length === 0)
178
+ i && console.warn(`No location data found for contentUUID: ${l}`), r.push({ contentUUID: l });
146
179
  else
147
- for (const u of n)
148
- l.push({
149
- contentUUID: p,
150
- locationUUID: u.locationUUID,
151
- created: u.created,
152
- size: u.size
180
+ for (const p of d)
181
+ r.push({
182
+ contentUUID: l,
183
+ locationUUID: p.locationUUID,
184
+ created: p.created,
185
+ size: p.size
153
186
  });
154
187
  }
155
- return l;
188
+ return r;
156
189
  }
157
190
  async _getGraphFiles(t, o) {
158
- const e = `PREFIX qcy: <${k.qcy}>
191
+ const e = `PREFIX qcy: <${M.qcy}>
159
192
  SELECT ?fc ?loc ?created ?fp ?size
160
193
  WHERE {
161
194
  ?fc a qcy:FileContent ;
@@ -166,28 +199,60 @@ WHERE {
166
199
  qcy:filePath ?fp .
167
200
  }`, i = await t.query(e, "application/sparql-results+json"), s = {};
168
201
  if (typeof i == "string") return s;
169
- for (const a of i.results.bindings) {
170
- const l = a.loc.value.split("/").at(-1) ?? "", c = a.fc.value.split("/").at(-1) ?? "", p = a.created.value, n = a.size.value;
171
- s[c] || (s[c] = []), s[c].push({ locationUUID: l, created: p, size: n });
202
+ for (const n of i.results.bindings) {
203
+ const r = n.loc.value.split("/").at(-1) ?? "", a = n.fc.value.split("/").at(-1) ?? "", l = n.created.value, d = n.size.value;
204
+ s[a] || (s[a] = []), s[a].push({ locationUUID: r, created: l, size: d });
172
205
  }
173
206
  return s;
174
207
  }
175
- async _uploadRdfMetadata(t, o, e) {
176
- if (!o.blob_name) throw new Error(`blob_name missing for ${t.relativePath}`);
177
- const i = B(
178
- o.blob_name,
179
- this._serviceId,
180
- t.locationUUID
181
- );
182
- if (!i.blob_name) throw new Error(`ttl blob_name missing for ${o.blob_name}`);
183
- const s = L(o, t.size), a = `${this._rdfBase}${i.space_id}/`, l = await G(a, s), c = await this._blob.uploadProcessed(
184
- i.blob_name,
185
- new Uint8Array(Buffer.from(l, "utf-8")),
186
- i
187
- );
188
- return !c && e && console.info(
189
- `Graph data for ${t.relativePath} already exists — skipping ⚠️`
190
- ), c;
208
+ async _initPendingBatch(t, o, e) {
209
+ this._pendingSpaceId = t, this._pendingItems = [];
210
+ const i = await Y(t);
211
+ i && i.items.length > 0 && (e && console.info(`Flushing ${i.items.length} pending file location(s) from previous sync ⏳`), await this._flushBatch(i.items, t, o, e));
212
+ const s = setInterval(() => {
213
+ this._drainPending(e).catch(
214
+ (n) => console.error("[CueSyncApi] Periodic flush failed:", n)
215
+ );
216
+ }, 6e4);
217
+ typeof s == "object" && typeof s.unref == "function" && s.unref(), this._flushTimer = s;
218
+ }
219
+ async _queueFileLocation(t) {
220
+ this._pendingItems.push(t), this._pendingSpaceId && await N({ spaceId: this._pendingSpaceId, items: this._pendingItems });
221
+ }
222
+ async _drainPending(t) {
223
+ if (!this._pendingSpaceId || this._pendingItems.length === 0) return;
224
+ const o = await this._auth.getToken();
225
+ o && await this._flushBatch(this._pendingItems, this._pendingSpaceId, o, t);
226
+ }
227
+ async _flushBatch(t, o, e, i) {
228
+ const s = [...t];
229
+ this._pendingSpaceId === o && (this._pendingItems = []), await V(o);
230
+ try {
231
+ for (let n = 0; n < s.length; n += F)
232
+ await this._postFssBatch(s.slice(n, n + F), o, e);
233
+ i && console.info(`Wrote ${s.length} file location(s) to commands API ✅`);
234
+ } catch (n) {
235
+ const r = [...s, ...this._pendingItems];
236
+ throw this._pendingItems = r, await N({ spaceId: o, items: r }), n;
237
+ }
238
+ }
239
+ async _postFssBatch(t, o, e) {
240
+ const i = await fetch(`${this._gatewayUrl}${Q}`, {
241
+ method: "POST",
242
+ headers: {
243
+ Authorization: `Bearer ${e}`,
244
+ "Content-Type": "application/json",
245
+ "x-project-id": o
246
+ },
247
+ body: JSON.stringify({ items: t })
248
+ });
249
+ if (!i.ok) {
250
+ const s = await i.text().catch(() => "");
251
+ throw new Error(`File structure batch POST failed: ${i.status} ${i.statusText}${s ? ` — ${s}` : ""}`);
252
+ }
253
+ }
254
+ _stopFlushTimer() {
255
+ this._flushTimer !== null && (clearInterval(this._flushTimer), this._flushTimer = null);
191
256
  }
192
257
  /**
193
258
  * Scans `localFiles` and returns a per-extension cost breakdown.
@@ -197,44 +262,51 @@ WHERE {
197
262
  * shown to the user before or after calling {@link sync}.
198
263
  */
199
264
  async scanCost(t) {
200
- _ || (_ = v()), await _;
201
- const o = await Promise.all(
202
- t.map(async (e) => ({
203
- originalPath: e.relativePath,
204
- data: new Uint8Array(await w(e.fullPath))
205
- }))
206
- );
207
- return q(o);
265
+ if ($ || ($ = k()), await $, !C) throw new Error("WASM scan function not initialised");
266
+ const o = 200, e = /* @__PURE__ */ new Map();
267
+ for (let i = 0; i < t.length; i += o) {
268
+ const s = t.slice(i, i + o), n = await Promise.all(
269
+ s.map(async (a) => ({
270
+ originalPath: a.relativePath,
271
+ data: new Uint8Array(await P(a.fullPath))
272
+ }))
273
+ ), r = C(n);
274
+ for (const a of r) {
275
+ const l = e.get(a.ext);
276
+ l ? (l.count += a.count, l.units += a.units, l.sizeMb += a.sizeMb) : e.set(a.ext, { ...a });
277
+ }
278
+ }
279
+ return Array.from(e.values());
208
280
  }
209
281
  _logProgress(t, o, e, i, s) {
210
282
  if (!s || o === 0 || t % Math.ceil(o / 100) !== 0) return;
211
- const a = Math.floor(t / o * 100);
283
+ const n = Math.floor(t / o * 100);
212
284
  console.info(
213
- `Progress: ${a}% (${t}/${o} files, ${e}/${i} bytes)`
285
+ `Progress: ${n}% (${t}/${o} files, ${e}/${i} bytes)`
214
286
  );
215
287
  }
216
288
  }
217
- const J = "spaces_raw_eu_west6", Z = "spaces_processed_eu_west6";
218
- class ft extends F {
289
+ const Z = "spaces_raw_eu_west6", tt = "spaces_processed_eu_west6";
290
+ class ut extends v {
219
291
  constructor(t) {
220
292
  super(t);
221
293
  }
222
294
  _buildApi(t) {
223
- const o = C(this._app, J), e = C(this._app, Z);
295
+ const o = b(this._app, Z), e = b(this._app, tt);
224
296
  if (this._isEmulator) {
225
- const a = this._endpoints.storageEmulatorHost, l = this._endpoints.storageEmulatorPort;
226
- R(o, a, l), R(e, a, l);
297
+ const r = this._endpoints.storageEmulatorHost, a = this._endpoints.storageEmulatorPort;
298
+ T(o, r, a), T(e, r, a);
227
299
  }
228
- const i = new z({ storageRaw: o, storageProcessed: e }), s = new X(this.auth, t, i, this._endpoints.gatewayUrl);
229
- return new D(this.auth, this._endpoints.gatewayUrl, t, s);
300
+ const i = new z({ storageRaw: o, storageProcessed: e }), s = new X(this.auth, t, i, this._endpoints.gatewayUrl), n = new O(this.auth, this._endpoints.gatewayUrl, t, s);
301
+ return s._bindApi(n), n;
230
302
  }
231
303
  }
232
304
  export {
233
- F as Cue,
234
- D as CueApi,
235
- ht as CueAuth,
236
- ft as CueNode,
237
- dt as CueProfile,
238
- _t as CueProjects,
305
+ v as Cue,
306
+ O as CueApi,
307
+ dt as CueAuth,
308
+ ut as CueNode,
309
+ _t as CueProfile,
310
+ gt as CueProjects,
239
311
  X as CueSyncApi
240
312
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qaecy/cue-sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -14,9 +14,6 @@
14
14
  "types": "./node.d.ts"
15
15
  }
16
16
  },
17
- "dependencies": {
18
- "dir-scanner-wasm": "file:../../../data/dir-scanner-wasm-0.1.0.tgz"
19
- },
20
17
  "peerDependencies": {
21
18
  "firebase": ">=10.0.0"
22
19
  }