@wishknish/knishio-client-ts 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -253,6 +253,73 @@ This document will explain both ways.
253
253
  console.log(fingerprintData);
254
254
  ```
255
255
 
256
+ ### DataBraid: Embedding Status (Observability)
257
+
258
+ When the validator has DataBraid embeddings enabled (`EMBEDDING_ENABLED=true`), the SDK can query the embedding state of meta assets. This allows apps to render UI indicators such as spinner badges for in-progress embeddings or completion checkmarks.
259
+
260
+ The SDK automatically detects whether the connected server supports this feature. If it does not, `queryEmbeddingStatus()` returns `null` instead of throwing an error.
261
+
262
+ - Query embedding status for a **single Meta Asset**:
263
+
264
+ ```typescript
265
+ import type { EmbeddingStatusItem } from '@wishknish/knishio-client-ts'
266
+
267
+ const response = await client.queryEmbeddingStatus({
268
+ metaType: 'Vehicle',
269
+ metaId: 'VIN-12345'
270
+ });
271
+
272
+ if (response) {
273
+ const items: EmbeddingStatusItem[] | null = response.payload();
274
+ // items[0] = {
275
+ // metaType: 'Vehicle',
276
+ // metaId: 'VIN-12345',
277
+ // state: 'COMPLETE', // 'PENDING' | 'STALE' | 'COMPLETE'
278
+ // totalMetas: 5, // Total meta rows for this instance
279
+ // embeddedCount: 5, // Rows with embeddings
280
+ // embeddedAt: 1713100800, // Unix timestamp of last embedding
281
+ // model: 'nomic-embed-text-v1.5'
282
+ // }
283
+ } else {
284
+ // Server does not support embedding status
285
+ }
286
+ ```
287
+
288
+ - **Bulk** embedding status for multiple assets in a single request:
289
+
290
+ ```typescript
291
+ const response = await client.queryEmbeddingStatus({
292
+ instances: [
293
+ { metaType: 'Vehicle', metaId: 'VIN-12345' },
294
+ { metaType: 'Vehicle', metaId: 'VIN-67890' },
295
+ { metaType: 'Profile', metaId: 'user_42' }
296
+ ]
297
+ });
298
+
299
+ if (response) {
300
+ const items = response.payload();
301
+ // items.length === 3, one per input, in the same order
302
+ for (const item of items!) {
303
+ console.log(`${item.metaType}:${item.metaId} → ${item.state}`);
304
+ }
305
+ }
306
+ ```
307
+
308
+ - **Capability detection** — check if the server supports a query field before calling it:
309
+
310
+ ```typescript
311
+ const supported: boolean = await client.hasQueryField('embeddingStatus');
312
+ // true if the server's GraphQL schema includes the field, false otherwise
313
+ // Result is cached per URI — no repeated network round-trips
314
+ ```
315
+
316
+ **Embedding States:**
317
+ | State | Meaning | Suggested UI |
318
+ |-------|---------|-------------|
319
+ | `PENDING` | No embeddings generated yet | Spinner / gray badge |
320
+ | `STALE` | Embeddings exist but model has changed | Refresh indicator |
321
+ | `COMPLETE` | All meta rows have current-model embeddings | Green checkmark |
322
+
256
323
  ## Advanced Usage: Working with Molecules
257
324
 
258
325
  For more granular control, you can work directly with Molecules:
@@ -419,6 +486,7 @@ This method involves individually building Atoms and Molecules, triggering the s
419
486
  1. `QueryBalance` and `QueryContinuId` -> returns a `Wallet` instance
420
487
  2. `QueryWalletList` -> returns a list of `Wallet` instances
421
488
  3. `MutationProposeMolecule`, `MutationRequestAuthorization`, `MutationCreateIdentifier`, `MutationLinkIdentifier`, `MutationClaimShadowWallet`, `MutationCreateToken`, `MutationRequestTokens`, and `MutationTransferTokens` -> returns molecule metadata
489
+ 4. `QueryEmbeddingStatus` -> returns an array of `EmbeddingStatusItem` objects (`{ metaType, metaId, state, totalMetas, embeddedCount, embeddedAt, model }`)
422
490
 
423
491
  ## Getting Help
424
492
 
package/dist/index.cjs CHANGED
@@ -1923,7 +1923,7 @@ function generateSecret(seed = null, length = CRYPTO_CONSTANTS.SECRET_LENGTH) {
1923
1923
  if (seed) {
1924
1924
  const sponge = new JsSHA__default.default("SHAKE256", "TEXT");
1925
1925
  sponge.update(seed);
1926
- return sponge.getHash("HEX", { outputLen: length * 2 }).toLowerCase();
1926
+ return sponge.getHash("HEX", { outputLen: length * 4 }).toLowerCase();
1927
1927
  } else {
1928
1928
  return randomString(length);
1929
1929
  }
@@ -4941,13 +4941,8 @@ var Molecule = class _Molecule {
4941
4941
  }) {
4942
4942
  const atomMeta = new AtomMeta(meta);
4943
4943
  atomMeta.addPolicy(policy);
4944
- const wallet = Wallet.create({
4945
- secret: this.secret,
4946
- bundle: this.sourceWallet.bundle,
4947
- token: "USER"
4948
- });
4949
4944
  this.addAtom(Atom.create({
4950
- wallet,
4945
+ wallet: this.sourceWallet,
4951
4946
  isotope: "R",
4952
4947
  metaType,
4953
4948
  metaId,
@@ -5932,6 +5927,20 @@ var GraphQLClient = class {
5932
5927
  encrypt: this.cipherLink
5933
5928
  });
5934
5929
  }
5930
+ /**
5931
+ * F-8a (cross-SDK parity, 2026-06-03): re-point the GraphQL-subscription WebSocket
5932
+ * without changing the HTTP endpoint. `setUri` only updates `serverUri`, leaving the
5933
+ * subscription socket pinned to whatever was passed at construction; this lets a
5934
+ * caller re-derive the socket when the endpoint changes.
5935
+ */
5936
+ setSocketUri(socketUri) {
5937
+ this.socketUri = socketUri;
5938
+ this.$__client = this.createUrqlClient({
5939
+ serverUri: this.serverUri,
5940
+ socket: { socketUri },
5941
+ encrypt: this.cipherLink
5942
+ });
5943
+ }
5935
5944
  socketDisconnect() {
5936
5945
  if (this.socketUri) {
5937
5946
  this.unsubscribeAll();
@@ -8832,9 +8841,91 @@ var QueryMetaTypeViaMolecule = class extends Query {
8832
8841
  }
8833
8842
  };
8834
8843
 
8844
+ // src/response/ResponseEmbeddingStatus.ts
8845
+ init_Response();
8846
+ var ResponseEmbeddingStatus = class extends exports.Response {
8847
+ /**
8848
+ * Class constructor
8849
+ */
8850
+ constructor({
8851
+ query,
8852
+ json
8853
+ }) {
8854
+ super({
8855
+ query,
8856
+ json,
8857
+ dataKey: "data.embeddingStatus"
8858
+ });
8859
+ }
8860
+ /**
8861
+ * Returns the array of embedding status items, or null if empty.
8862
+ */
8863
+ payload() {
8864
+ const items = this.data();
8865
+ if (!items || !Array.isArray(items) || items.length === 0) {
8866
+ return null;
8867
+ }
8868
+ return items;
8869
+ }
8870
+ };
8871
+ var QueryEmbeddingStatus = class extends Query {
8872
+ /**
8873
+ * Create new QueryEmbeddingStatus instance
8874
+ */
8875
+ constructor(graphQLClient, knishIOClient) {
8876
+ super(graphQLClient, knishIOClient);
8877
+ this.$__query = core.gql`query( $metaType: String, $metaId: String, $instances: [EmbeddingStatusInput!] ) {
8878
+ embeddingStatus( metaType: $metaType, metaId: $metaId, instances: $instances ) {
8879
+ metaType,
8880
+ metaId,
8881
+ state,
8882
+ totalMetas,
8883
+ embeddedCount,
8884
+ embeddedAt,
8885
+ model
8886
+ }
8887
+ }`;
8888
+ }
8889
+ /**
8890
+ * Builds a GraphQL-friendly variables object for embedding status queries.
8891
+ *
8892
+ * Single mode: createVariables({ metaType: 'product', metaId: 'SKU-001' })
8893
+ * Bulk mode: createVariables({ instances: [{ metaType: 'product', metaId: 'SKU-001' }, ...] })
8894
+ */
8895
+ static createVariables({
8896
+ metaType = null,
8897
+ metaId = null,
8898
+ instances = null
8899
+ } = {}) {
8900
+ const variables = {};
8901
+ if (instances && instances.length > 0) {
8902
+ variables.instances = instances;
8903
+ }
8904
+ if (metaType) {
8905
+ variables.metaType = metaType;
8906
+ }
8907
+ if (metaId) {
8908
+ variables.metaId = metaId;
8909
+ }
8910
+ return variables;
8911
+ }
8912
+ /**
8913
+ * Returns a Response object
8914
+ */
8915
+ createResponse(json) {
8916
+ return new ResponseEmbeddingStatus({
8917
+ query: this,
8918
+ json
8919
+ });
8920
+ }
8921
+ };
8922
+
8835
8923
  // src/response/ResponseProposeMolecule.ts
8836
8924
  init_Response();
8837
8925
  init_Dot();
8926
+ init_MolecularHashMismatchException();
8927
+ init_SignatureMismatchException();
8928
+ init_AtomIndexException();
8838
8929
  var ResponseProposeMolecule = class extends exports.Response {
8839
8930
  $__clientMolecule;
8840
8931
  /**
@@ -8906,6 +8997,47 @@ var ResponseProposeMolecule = class extends exports.Response {
8906
8997
  reason() {
8907
8998
  return exports.Dot.get(this.data(), "reason", "Invalid response from server");
8908
8999
  }
9000
+ /**
9001
+ * Map this rejection to a typed SDK exception when the failure mode is one
9002
+ * we know about. Returns null on success or for rejections we don't have a
9003
+ * typed class for.
9004
+ *
9005
+ * This is the single place pattern-matching against validator reason strings
9006
+ * lives — consumers (KnishIOClient cache invalidation, callers needing to
9007
+ * branch on failure mode) can `instanceof`-switch on the result instead.
9008
+ * Future validator versions can rephrase reasons (or populate a structured
9009
+ * field in the response payload) without breaking callers.
9010
+ */
9011
+ toException() {
9012
+ if (this.success()) return null;
9013
+ const reason = this.reason();
9014
+ const lc = reason.toLowerCase();
9015
+ if (/molecularhashmismatch|hash.*mismatch/.test(lc)) {
9016
+ return new exports.MolecularHashMismatchException(reason, {
9017
+ details: { reason },
9018
+ code: "HASH_MISMATCH"
9019
+ });
9020
+ }
9021
+ if (/ots.*position.*reuse|position.*already.*used|ots.*verification/.test(lc)) {
9022
+ return new exports.SignatureMismatchException(reason, {
9023
+ details: { reason },
9024
+ code: "OTS_VERIFICATION_FAILED"
9025
+ });
9026
+ }
9027
+ if (/continuid.*chain|previousposition|chain.*violation/.test(lc)) {
9028
+ return new exports.AtomIndexException(reason, {
9029
+ details: { reason },
9030
+ code: "INDEX_CONFLICT"
9031
+ });
9032
+ }
9033
+ if (/signature.*verification|signature.*invalid/.test(lc)) {
9034
+ return new exports.SignatureMismatchException(reason, {
9035
+ details: { reason },
9036
+ code: "VERIFICATION_FAILED"
9037
+ });
9038
+ }
9039
+ return null;
9040
+ }
8909
9041
  /**
8910
9042
  * Returns payload object
8911
9043
  * Matches JavaScript SDK payload method exactly
@@ -9943,6 +10075,13 @@ var KnishIOClient = class {
9943
10075
  $__remainderWallet = null;
9944
10076
  lastMoleculeQuery = null;
9945
10077
  abortControllers = /* @__PURE__ */ new Map();
10078
+ $__capabilityCache = {};
10079
+ // Promise-chain mutex serializing MutationProposeMolecule submissions on this
10080
+ // client. Without it, concurrent createMolecule() calls both query the same
10081
+ // ContinuID position and both sign with it — the second is rejected with
10082
+ // OTS position reuse. Auth-token flow (MutationRequestAuthorization) inherits
10083
+ // from MutationProposeMolecule so it's covered by the same lock.
10084
+ $__moleculeChain = Promise.resolve();
9946
10085
  /**
9947
10086
  * Enhanced constructor with standardized configuration validation (Phase 2 Enhancement)
9948
10087
  */
@@ -10067,6 +10206,7 @@ var KnishIOClient = class {
10067
10206
  this.$__authToken = null;
10068
10207
  this.$__remainderWallet = null;
10069
10208
  this.lastMoleculeQuery = null;
10209
+ this.$__capabilityCache = {};
10070
10210
  }
10071
10211
  /**
10072
10212
  * Get the GraphQL client
@@ -10087,6 +10227,19 @@ var KnishIOClient = class {
10087
10227
  this.$__client.setUri(this.getRandomUri());
10088
10228
  }
10089
10229
  }
10230
+ /**
10231
+ * Sets the WebSocket (subscription) endpoint for this session.
10232
+ *
10233
+ * F-8a (cross-SDK parity, 2026-06-03): `setUri` only updates the HTTP endpoint; the
10234
+ * subscription socket is built once from the `socket.socketUri` passed at
10235
+ * construction. This lets a caller re-point the socket when the endpoint changes
10236
+ * (mirrors the JS SDK's `setSocketUri`).
10237
+ */
10238
+ setSocketUri(socketUri) {
10239
+ if (this.$__client && "setSocketUri" in this.$__client) {
10240
+ this.$__client.setSocketUri(socketUri);
10241
+ }
10242
+ }
10090
10243
  /**
10091
10244
  * Gets the Knish.IO server URIs
10092
10245
  */
@@ -10210,6 +10363,23 @@ var KnishIOClient = class {
10210
10363
  this.lastMoleculeQuery = mutation;
10211
10364
  return mutation;
10212
10365
  }
10366
+ /**
10367
+ * Serializes the given async work behind a per-client promise chain. Used to
10368
+ * guarantee that at most one MutationProposeMolecule submission runs at a
10369
+ * time on this client — query position, sign, submit, observe response must
10370
+ * complete before the next one starts, or two callers race for the same OTS
10371
+ * position and the second gets rejected.
10372
+ *
10373
+ * `.then(fn, fn)` runs fn whether the previous holder resolved or rejected;
10374
+ * the queue's `.catch` swallows the rejection so a single failure doesn't
10375
+ * poison every subsequent caller, while the rejection still propagates to
10376
+ * the caller whose fn threw.
10377
+ */
10378
+ withMoleculeLock(fn) {
10379
+ const result = this.$__moleculeChain.then(fn, fn);
10380
+ this.$__moleculeChain = result.catch(() => void 0);
10381
+ return result;
10382
+ }
10213
10383
  /**
10214
10384
  * Executes a query or mutation
10215
10385
  */
@@ -10222,8 +10392,34 @@ var KnishIOClient = class {
10222
10392
  encrypt: this.$__encrypt
10223
10393
  });
10224
10394
  }
10395
+ if (query instanceof MutationProposeMolecule) {
10396
+ return await this.withMoleculeLock(async () => {
10397
+ const response = await query.execute({ variables: variables || {} });
10398
+ this.handlePositionDrift(response);
10399
+ return response;
10400
+ });
10401
+ }
10225
10402
  return await query.execute({ variables: variables || {} });
10226
10403
  }
10404
+ /**
10405
+ * When a ProposeMolecule submission is rejected for a position-related
10406
+ * reason (OTS reuse, ContinuID chain violation, molecular hash mismatch),
10407
+ * the cached remainder wallet and lastMoleculeQuery are stale — the
10408
+ * validator's chain has advanced past what we know. Clear them so the next
10409
+ * createMolecule call re-queries queryContinuId for the authoritative
10410
+ * position. Other failure modes (network, malformed meta, bad signature
10411
+ * bytes) don't imply drift; leave cached state alone.
10412
+ */
10413
+ handlePositionDrift(response) {
10414
+ if (!(response instanceof ResponseProposeMolecule)) return;
10415
+ const exc = response.toException();
10416
+ if (!exc) return;
10417
+ if (exc instanceof exports.MolecularHashMismatchException || exc instanceof exports.AtomIndexException || exc instanceof exports.SignatureMismatchException && exc.code === "OTS_VERIFICATION_FAILED") {
10418
+ this.$__remainderWallet = null;
10419
+ this.lastMoleculeQuery = null;
10420
+ this.log("warn", `KnishIOClient::executeQuery() - position drift detected (${exc.name}/${exc.code}); cleared cached remainder wallet`);
10421
+ }
10422
+ }
10227
10423
  /**
10228
10424
  * Sets the secret for this session
10229
10425
  */
@@ -10603,6 +10799,63 @@ var KnishIOClient = class {
10603
10799
  }
10604
10800
  return response;
10605
10801
  }
10802
+ /**
10803
+ * Probes the connected server to check whether it supports a named root query field.
10804
+ * Result is cached per URI so the network round-trip happens at most once per URI.
10805
+ *
10806
+ * Uses GraphQL introspection which is universally supported by spec-compliant servers.
10807
+ *
10808
+ * @param fieldName - The root Query field name to check (e.g. 'embeddingStatus')
10809
+ * @returns true if the server schema includes the field, false otherwise
10810
+ */
10811
+ async hasQueryField(fieldName) {
10812
+ const uri = this.$__client.getUri();
10813
+ const cacheKey = `${uri}::${fieldName}`;
10814
+ if (typeof this.$__capabilityCache[cacheKey] === "boolean") {
10815
+ return this.$__capabilityCache[cacheKey];
10816
+ }
10817
+ try {
10818
+ const result = await this.$__client.query({
10819
+ query: "{ __schema { queryType { fields { name } } } }",
10820
+ variables: {}
10821
+ });
10822
+ const data = result?.data;
10823
+ const fields = data?.__schema?.queryType?.fields || [];
10824
+ const supported = fields.some((f) => f.name === fieldName);
10825
+ this.$__capabilityCache[cacheKey] = supported;
10826
+ return supported;
10827
+ } catch (err) {
10828
+ this.log("warn", `KnishIOClient::hasQueryField() - Capability probe for '${fieldName}' failed: ${err.message}`);
10829
+ this.$__capabilityCache[cacheKey] = false;
10830
+ return false;
10831
+ }
10832
+ }
10833
+ /**
10834
+ * Queries embedding status for one or more meta instances (DataBraid observability).
10835
+ *
10836
+ * If the connected server does not support the embeddingStatus query,
10837
+ * returns null without throwing an error (graceful degradation).
10838
+ *
10839
+ * Single mode: queryEmbeddingStatus({ metaType: 'product', metaId: 'SKU-001' })
10840
+ * Bulk mode: queryEmbeddingStatus({ instances: [{ metaType: 'product', metaId: 'SKU-001' }, ...] })
10841
+ *
10842
+ * @returns Response with payload(), or null if the server does not support this query
10843
+ */
10844
+ async queryEmbeddingStatus({
10845
+ metaType = null,
10846
+ metaId = null,
10847
+ instances = null
10848
+ }) {
10849
+ this.log("info", `KnishIOClient::queryEmbeddingStatus() - Checking embedding status for metaType: ${metaType || "(bulk)"}...`);
10850
+ const supported = await this.hasQueryField("embeddingStatus");
10851
+ if (!supported) {
10852
+ this.log("warn", "KnishIOClient::queryEmbeddingStatus() - Server does not support embeddingStatus query. Returning null.");
10853
+ return null;
10854
+ }
10855
+ const query = this.createQuery(QueryEmbeddingStatus);
10856
+ const variables = QueryEmbeddingStatus.createVariables({ metaType, metaId, instances });
10857
+ return this.executeQuery(query, variables);
10858
+ }
10606
10859
  /**
10607
10860
  * Query cascading meta instances for batchId
10608
10861
  */
@@ -11594,6 +11847,7 @@ exports.QueryAtom = QueryAtom;
11594
11847
  exports.QueryBalance = QueryBalance;
11595
11848
  exports.QueryBatch = QueryBatch;
11596
11849
  exports.QueryContinuId = QueryContinuId;
11850
+ exports.QueryEmbeddingStatus = QueryEmbeddingStatus;
11597
11851
  exports.QueryMetaType = QueryMetaType;
11598
11852
  exports.QueryMetaTypeViaAtom = QueryMetaTypeViaAtom;
11599
11853
  exports.QueryWalletBundle = QueryWalletBundle;
@@ -11605,6 +11859,7 @@ exports.ResponseContinuId = ResponseContinuId;
11605
11859
  exports.ResponseCreateMeta = ResponseCreateMeta;
11606
11860
  exports.ResponseCreateToken = ResponseCreateToken;
11607
11861
  exports.ResponseCreateWallet = ResponseCreateWallet;
11862
+ exports.ResponseEmbeddingStatus = ResponseEmbeddingStatus;
11608
11863
  exports.ResponseMetaType = ResponseMetaType;
11609
11864
  exports.ResponseMetaTypeViaAtom = ResponseMetaTypeViaAtom;
11610
11865
  exports.ResponsePeering = ResponsePeering;