@aouda/client 0.1.14 → 0.1.15

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/dist/cli/index.js CHANGED
@@ -392,7 +392,7 @@ import { fileURLToPath } from "url";
392
392
  // package.json
393
393
  var package_default = {
394
394
  name: "@aouda/client",
395
- version: "0.1.14",
395
+ version: "0.1.15",
396
396
  description: "Official TypeScript/JavaScript client library for Aouda",
397
397
  type: "module",
398
398
  main: "./dist/index.cjs",
@@ -507,43 +507,44 @@ var AoudaResponseError = class extends AoudaError {
507
507
  }
508
508
  };
509
509
  var AoudaApiError = class extends AoudaError {
510
- constructor(message, code, statusCode, details, requestId, retryAfterSeconds) {
510
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
511
511
  super(message);
512
512
  this.code = code;
513
513
  this.statusCode = statusCode;
514
514
  this.details = details;
515
515
  this.requestId = requestId;
516
516
  this.retryAfterSeconds = retryAfterSeconds;
517
+ this.token = token;
517
518
  this.name = "AoudaApiError";
518
519
  }
519
520
  };
520
521
  var AoudaNotFoundError = class extends AoudaApiError {
521
- constructor(message, code, statusCode, details, requestId) {
522
- super(message, code, statusCode, details, requestId);
522
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
523
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
523
524
  this.name = "AoudaNotFoundError";
524
525
  }
525
526
  };
526
527
  var AoudaConflictError = class extends AoudaApiError {
527
- constructor(message, code, statusCode, details, requestId) {
528
- super(message, code, statusCode, details, requestId);
528
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
529
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
529
530
  this.name = "AoudaConflictError";
530
531
  }
531
532
  };
532
533
  var AoudaValidationError = class extends AoudaApiError {
533
- constructor(message, code, statusCode, details, requestId) {
534
- super(message, code, statusCode, details, requestId);
534
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
535
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
535
536
  this.name = "AoudaValidationError";
536
537
  }
537
538
  };
538
539
  var AoudaServerError = class extends AoudaApiError {
539
- constructor(message, code, statusCode, details, requestId, retryAfterSeconds) {
540
- super(message, code, statusCode, details, requestId, retryAfterSeconds);
540
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
541
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
541
542
  this.name = "AoudaServerError";
542
543
  }
543
544
  };
544
545
  var AoudaAuthenticationError = class extends AoudaApiError {
545
- constructor(message, code, statusCode, details, requestId) {
546
- super(message, code, statusCode, details, requestId);
546
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
547
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
547
548
  this.name = "AoudaAuthenticationError";
548
549
  }
549
550
  };
@@ -1339,7 +1340,7 @@ function buildHandle(jobId, commitResp, wasResumed, transport, database, signal)
1339
1340
  rowsDurablyCommitted: commitResp.rowsLoaded,
1340
1341
  segmentsCreated: commitResp.segmentsCreated,
1341
1342
  committedAtUtc: commitResp.committedAtUtc,
1342
- walPosition: commitResp.walPosition,
1343
+ token: commitResp.token,
1343
1344
  writeConcernSatisfied: commitResp.writeConcernAchieved ?? "acknowledged",
1344
1345
  writeConcernTimedOut: commitResp.writeConcernTimedOut,
1345
1346
  wasResumed,
@@ -1465,6 +1466,7 @@ var PROTOCOL_VERSION_HEADER = "X-Aouda-Protocol-Version";
1465
1466
  var PROTOCOL_VERSION = "1";
1466
1467
  var CONTENT_TYPE_JSON2 = "application/json";
1467
1468
  var REQUEST_ID_HEADER2 = "X-Request-Id";
1469
+ var TOKEN_HEADER = "X-Aouda-Token";
1468
1470
  var ERROR_CODE_MAP = {
1469
1471
  TABLE_NOT_FOUND: AoudaNotFoundError,
1470
1472
  COLUMN_NOT_FOUND: AoudaNotFoundError,
@@ -1504,7 +1506,12 @@ var ERROR_CODE_MAP = {
1504
1506
  AUTH_IDENTITY_INVALID: AoudaValidationError,
1505
1507
  AUTH_IDENTITY_NOT_FOUND: AoudaValidationError,
1506
1508
  BULK_LOAD_TRANSFORM_INTENT_REQUIRED: AoudaValidationError,
1507
- BULK_LOAD_TRANSFORM_INTENT_CONFLICT: AoudaValidationError
1509
+ BULK_LOAD_TRANSFORM_INTENT_CONFLICT: AoudaValidationError,
1510
+ TOKEN_MALFORMED: AoudaValidationError,
1511
+ TOKEN_FOREIGN_DATABASE: AoudaValidationError,
1512
+ TOKEN_EPOCH_SUPERSEDED: AoudaConflictError,
1513
+ TOKEN_UNSATISFIED: AoudaConflictError,
1514
+ TOKEN_FETCH_PRIMARY: AoudaApiError
1508
1515
  };
1509
1516
  function createComposedAbortController(...signals) {
1510
1517
  const controller = new AbortController();
@@ -1541,7 +1548,7 @@ function createApiError(statusCode, statusText, body, retryAfterHeader) {
1541
1548
  const requestId = body.requestId;
1542
1549
  const retryAfterSeconds = parseRetryAfterSeconds(retryAfterHeader ?? null);
1543
1550
  const Ctor = ERROR_CODE_MAP[code] ?? AoudaApiError;
1544
- return new Ctor(message, code, statusCode, details, requestId, retryAfterSeconds);
1551
+ return new Ctor(message, code, statusCode, details, requestId, retryAfterSeconds, body.token);
1545
1552
  }
1546
1553
  var HttpTransport = class {
1547
1554
  constructor(options) {
@@ -1553,6 +1560,29 @@ var HttpTransport = class {
1553
1560
  ...options.defaultHeaders
1554
1561
  };
1555
1562
  this.abortController = new AbortController();
1563
+ this.database = options.database;
1564
+ this.store = options.consistencyTokenStore;
1565
+ }
1566
+ presentTokenHeaders(path4, headers) {
1567
+ if (this.store == null || this.database == null || isAuthPath(path4)) {
1568
+ return;
1569
+ }
1570
+ const token = this.store.get(this.database);
1571
+ if (token != null && token.length > 0) {
1572
+ headers[TOKEN_HEADER] = token;
1573
+ }
1574
+ }
1575
+ observeResponse(_path, response, bodyText, errorCode) {
1576
+ if (this.store == null || this.database == null) {
1577
+ return;
1578
+ }
1579
+ if (errorCode === "TOKEN_MALFORMED" || errorCode === "TOKEN_FOREIGN_DATABASE") {
1580
+ return;
1581
+ }
1582
+ const header = response.headers.get(TOKEN_HEADER);
1583
+ const bodyToken = readBodyToken(bodyText);
1584
+ const token = header != null && header.trim().length > 0 ? header : bodyToken;
1585
+ this.store.observe(this.database, token);
1556
1586
  }
1557
1587
  /**
1558
1588
  * Abort all in-flight and future requests (e.g. on client disconnect).
@@ -1590,6 +1620,7 @@ var HttpTransport = class {
1590
1620
  ...this.defaultHeaders,
1591
1621
  ...config.headers
1592
1622
  };
1623
+ this.presentTokenHeaders(config.path, headers);
1593
1624
  if (config.body !== void 0 && config.body !== null) {
1594
1625
  headers["Content-Type"] = CONTENT_TYPE_JSON2;
1595
1626
  }
@@ -1609,6 +1640,7 @@ var HttpTransport = class {
1609
1640
  const text = await response.text();
1610
1641
  if (config.allowStatuses?.includes(response.status) && text) {
1611
1642
  try {
1643
+ this.observeResponse(config.path, response, text);
1612
1644
  return JSON.parse(text);
1613
1645
  } catch {
1614
1646
  }
@@ -1622,15 +1654,19 @@ var HttpTransport = class {
1622
1654
  }
1623
1655
  if (response.status === 401) {
1624
1656
  const code = errorBody?.code ?? "AUTH_TOKEN_MISSING";
1657
+ this.observeResponse(config.path, response, text, code);
1625
1658
  throw new AoudaAuthenticationError(
1626
1659
  errorBody?.error ?? "Unauthorized",
1627
1660
  code,
1628
1661
  401,
1629
1662
  errorBody?.details,
1630
- errorBody?.requestId
1663
+ errorBody?.requestId,
1664
+ void 0,
1665
+ errorBody?.token
1631
1666
  );
1632
1667
  }
1633
1668
  if (errorBody?.code != null) {
1669
+ this.observeResponse(config.path, response, text, errorBody.code);
1634
1670
  throw createApiError(
1635
1671
  response.status,
1636
1672
  response.statusText,
@@ -1638,6 +1674,7 @@ var HttpTransport = class {
1638
1674
  response.headers.get("Retry-After")
1639
1675
  );
1640
1676
  }
1677
+ this.observeResponse(config.path, response, text);
1641
1678
  throw new AoudaResponseError(
1642
1679
  errorBody?.error ?? `${response.status} ${response.statusText}`,
1643
1680
  response.status,
@@ -1645,6 +1682,7 @@ var HttpTransport = class {
1645
1682
  );
1646
1683
  }
1647
1684
  const responseText = await response.text();
1685
+ this.observeResponse(config.path, response, responseText);
1648
1686
  if (config.rawText) {
1649
1687
  return responseText;
1650
1688
  }
@@ -1705,6 +1743,7 @@ var HttpTransport = class {
1705
1743
  ...this.defaultHeaders,
1706
1744
  ...config.headers
1707
1745
  };
1746
+ this.presentTokenHeaders(config.path, headers);
1708
1747
  if (config.rawBodyStr === void 0 && config.body !== void 0 && config.body !== null) {
1709
1748
  headers["Content-Type"] = CONTENT_TYPE_JSON2;
1710
1749
  }
@@ -1721,6 +1760,7 @@ var HttpTransport = class {
1721
1760
  try {
1722
1761
  const response = await localNetworkFetch(url, init);
1723
1762
  if (response.ok || config.allowStatuses?.includes(response.status)) {
1763
+ this.observeResponse(config.path, response, void 0);
1724
1764
  return response;
1725
1765
  }
1726
1766
  const text = await response.text();
@@ -1733,15 +1773,19 @@ var HttpTransport = class {
1733
1773
  }
1734
1774
  if (response.status === 401) {
1735
1775
  const code = errorBody?.code ?? "AUTH_TOKEN_MISSING";
1776
+ this.observeResponse(config.path, response, text, code);
1736
1777
  throw new AoudaAuthenticationError(
1737
1778
  errorBody?.error ?? "Unauthorized",
1738
1779
  code,
1739
1780
  401,
1740
1781
  errorBody?.details,
1741
- errorBody?.requestId
1782
+ errorBody?.requestId,
1783
+ void 0,
1784
+ errorBody?.token
1742
1785
  );
1743
1786
  }
1744
1787
  if (errorBody?.code != null) {
1788
+ this.observeResponse(config.path, response, text, errorBody.code);
1745
1789
  throw createApiError(
1746
1790
  response.status,
1747
1791
  response.statusText,
@@ -1749,6 +1793,7 @@ var HttpTransport = class {
1749
1793
  response.headers.get("Retry-After")
1750
1794
  );
1751
1795
  }
1796
+ this.observeResponse(config.path, response, text);
1752
1797
  throw new AoudaResponseError(
1753
1798
  errorBody?.error ?? `${response.status} ${response.statusText}`,
1754
1799
  response.status,
@@ -1903,6 +1948,47 @@ var HttpTransport = class {
1903
1948
  });
1904
1949
  }
1905
1950
  };
1951
+ function isAuthPath(path4) {
1952
+ let p = path4;
1953
+ const q = p.indexOf("?");
1954
+ if (q >= 0) {
1955
+ p = p.slice(0, q);
1956
+ }
1957
+ if (/^https?:\/\//i.test(p)) {
1958
+ try {
1959
+ p = new URL(p).pathname;
1960
+ } catch {
1961
+ }
1962
+ }
1963
+ const lower = p.toLowerCase();
1964
+ if (lower === "/api/auth" || lower.startsWith("/api/auth/")) {
1965
+ return true;
1966
+ }
1967
+ const prefix = "/api/databases/";
1968
+ if (!lower.startsWith(prefix)) {
1969
+ return false;
1970
+ }
1971
+ const rest = lower.slice(prefix.length);
1972
+ const slash = rest.indexOf("/");
1973
+ if (slash < 0) {
1974
+ return false;
1975
+ }
1976
+ const afterDb = rest.slice(slash + 1);
1977
+ return afterDb === "auth" || afterDb.startsWith("auth/");
1978
+ }
1979
+ function readBodyToken(bodyText) {
1980
+ if (bodyText == null || bodyText.trim().length === 0) {
1981
+ return void 0;
1982
+ }
1983
+ try {
1984
+ const parsed = JSON.parse(bodyText);
1985
+ if (parsed !== null && typeof parsed === "object" && "token" in parsed && typeof parsed.token === "string") {
1986
+ return parsed.token;
1987
+ }
1988
+ } catch {
1989
+ }
1990
+ return void 0;
1991
+ }
1906
1992
 
1907
1993
  // src/resilience/retry.ts
1908
1994
  var DEFAULT_MAX_RETRIES = 3;
@@ -2335,6 +2421,48 @@ function databasePath2(db) {
2335
2421
  return `/api/databases/${encodeURIComponent(db)}`;
2336
2422
  }
2337
2423
 
2424
+ // src/consistency-token-store.ts
2425
+ var MemoryConsistencyTokenStore = class {
2426
+ constructor() {
2427
+ this.tokens = /* @__PURE__ */ new Map();
2428
+ }
2429
+ get(database) {
2430
+ if (typeof database !== "string" || database.trim().length === 0) {
2431
+ throw new Error("database must be a non-empty string");
2432
+ }
2433
+ return this.tokens.get(normalizeKey(database));
2434
+ }
2435
+ observe(database, token) {
2436
+ if (typeof database !== "string" || database.trim().length === 0) {
2437
+ throw new Error("database must be a non-empty string");
2438
+ }
2439
+ if (token == null || token.trim().length === 0) {
2440
+ return;
2441
+ }
2442
+ const key = normalizeKey(database);
2443
+ const stored = this.tokens.get(key);
2444
+ if (stored === void 0 || compareOrdinal(token, stored) > 0) {
2445
+ this.tokens.set(key, token);
2446
+ }
2447
+ }
2448
+ };
2449
+ function compareOrdinal(left, right) {
2450
+ if (left === right) return 0;
2451
+ return left < right ? -1 : 1;
2452
+ }
2453
+ function maxToken(left, right) {
2454
+ if (left == null || left.length === 0) {
2455
+ return right == null || right.length === 0 ? void 0 : right;
2456
+ }
2457
+ if (right == null || right.length === 0) {
2458
+ return left;
2459
+ }
2460
+ return compareOrdinal(left, right) >= 0 ? left : right;
2461
+ }
2462
+ function normalizeKey(database) {
2463
+ return database.toLowerCase();
2464
+ }
2465
+
2338
2466
  // src/streaming/subscription.ts
2339
2467
  var AsyncEventQueue = class {
2340
2468
  constructor() {
@@ -2391,7 +2519,7 @@ var AsyncEventQueue = class {
2391
2519
  }
2392
2520
  };
2393
2521
  var TableSubscription = class {
2394
- constructor(transport, identity, options = {}, onWarnings) {
2522
+ constructor(transport, identity, options = {}, onWarnings, store, database) {
2395
2523
  this._queue = new AsyncEventQueue();
2396
2524
  this._active = true;
2397
2525
  this._started = false;
@@ -2407,6 +2535,11 @@ var TableSubscription = class {
2407
2535
  this._onError = options.onError;
2408
2536
  this._onWarnings = onWarnings;
2409
2537
  this._conflate = options.conflate;
2538
+ this._atLeast = options.atLeast;
2539
+ this._waitMs = options.waitMs;
2540
+ this._onExceeded = options.onExceeded;
2541
+ this._store = store;
2542
+ this._database = database;
2410
2543
  this._reconnectHandlerKey = `${this.id}::reconnect`;
2411
2544
  }
2412
2545
  get lastVersion() {
@@ -2471,7 +2604,7 @@ var TableSubscription = class {
2471
2604
  id: this.id
2472
2605
  };
2473
2606
  if (this._identity.kind === "named") {
2474
- message.hash = this._identity.hash;
2607
+ message.name = this._identity.name;
2475
2608
  if (this._identity.args !== void 0) {
2476
2609
  message.args = this._identity.args;
2477
2610
  }
@@ -2490,8 +2623,30 @@ var TableSubscription = class {
2490
2623
  if (this._conflate !== void 0) {
2491
2624
  message.conflate = this._conflate;
2492
2625
  }
2626
+ const pin = this._resolvePin();
2627
+ if (pin !== void 0) {
2628
+ message.at_least = pin;
2629
+ }
2630
+ if (this._waitMs !== void 0) {
2631
+ message.wait_ms = this._waitMs;
2632
+ }
2633
+ if (this._onExceeded !== void 0) {
2634
+ message.on_exceeded = this._onExceeded;
2635
+ }
2493
2636
  await this._transport.send(message);
2494
2637
  }
2638
+ _resolvePin() {
2639
+ if (this._atLeast !== void 0 && this._store != null && this._database != null) {
2640
+ this._store.observe(this._database, this._atLeast);
2641
+ }
2642
+ const stored = this._store != null && this._database != null ? this._store.get(this._database) : void 0;
2643
+ return maxToken(this._atLeast, stored);
2644
+ }
2645
+ _observeToken(token) {
2646
+ if (token !== void 0 && this._store != null && this._database != null) {
2647
+ this._store.observe(this._database, token);
2648
+ }
2649
+ }
2495
2650
  _handleMessage(message) {
2496
2651
  if (!this._active) {
2497
2652
  return;
@@ -2518,6 +2673,7 @@ var TableSubscription = class {
2518
2673
  }
2519
2674
  _handleSnapshotPage(message) {
2520
2675
  this._pendingSnapshotRows.push(...message.rows);
2676
+ this._observeToken(message.token);
2521
2677
  }
2522
2678
  _handleSnapshotComplete(message) {
2523
2679
  this._lastVersion = message.version;
@@ -2535,6 +2691,10 @@ var TableSubscription = class {
2535
2691
  if (message.total_matches !== void 0) {
2536
2692
  snapshot.totalMatches = message.total_matches;
2537
2693
  }
2694
+ if (message.token !== void 0) {
2695
+ snapshot.token = message.token;
2696
+ this._observeToken(message.token);
2697
+ }
2538
2698
  this._queue.push(snapshot);
2539
2699
  }
2540
2700
  async _handleGap(message) {
@@ -2560,6 +2720,10 @@ var TableSubscription = class {
2560
2720
  if (message.values_skipped !== void 0) {
2561
2721
  event.values_skipped = message.values_skipped;
2562
2722
  }
2723
+ if (message.token !== void 0) {
2724
+ event.token = message.token;
2725
+ this._observeToken(message.token);
2726
+ }
2563
2727
  this._onChange?.(event);
2564
2728
  this._queue.push(event);
2565
2729
  }
@@ -2904,11 +3068,12 @@ var TableQuery = class _TableQuery {
2904
3068
  * @param state - Optional initial state (used for immutable chaining).
2905
3069
  * @internal Use `client.table()` to create queries.
2906
3070
  */
2907
- constructor(transport, tableName, database, state, getWebSocketTransport) {
3071
+ constructor(transport, tableName, database, state, getWebSocketTransport, store) {
2908
3072
  this.transport = transport;
2909
3073
  this.tableName = tableName;
2910
3074
  this.database = database;
2911
3075
  this.getWebSocketTransport = getWebSocketTransport;
3076
+ this.store = store;
2912
3077
  this.state = state ?? {
2913
3078
  predicates: [],
2914
3079
  groupClauses: [],
@@ -2924,6 +3089,16 @@ var TableQuery = class _TableQuery {
2924
3089
  isDistinct: false
2925
3090
  };
2926
3091
  }
3092
+ withState(state) {
3093
+ return new _TableQuery(
3094
+ this.transport,
3095
+ this.tableName,
3096
+ this.database,
3097
+ state,
3098
+ this.getWebSocketTransport,
3099
+ this.store
3100
+ );
3101
+ }
2927
3102
  where(column, operator, value) {
2928
3103
  const newPredicates = buildWherePredicates(column, operator, value);
2929
3104
  return new _TableQuery(
@@ -2934,7 +3109,8 @@ var TableQuery = class _TableQuery {
2934
3109
  ...this.state,
2935
3110
  predicates: [...this.state.predicates, ...newPredicates]
2936
3111
  },
2937
- this.getWebSocketTransport
3112
+ this.getWebSocketTransport,
3113
+ this.store
2938
3114
  );
2939
3115
  }
2940
3116
  /**
@@ -2953,7 +3129,7 @@ var TableQuery = class _TableQuery {
2953
3129
  return new _TableQuery(this.transport, this.tableName, this.database, {
2954
3130
  ...this.state,
2955
3131
  groupClauses: [...this.state.groupClauses, sub]
2956
- }, this.getWebSocketTransport);
3132
+ }, this.getWebSocketTransport, this.store);
2957
3133
  }
2958
3134
  /**
2959
3135
  * Sets the primary sort column for the query.
@@ -2978,7 +3154,7 @@ var TableQuery = class _TableQuery {
2978
3154
  return new _TableQuery(this.transport, this.tableName, this.database, {
2979
3155
  ...this.state,
2980
3156
  orderByClauses: [orderByClause]
2981
- }, this.getWebSocketTransport);
3157
+ }, this.getWebSocketTransport, this.store);
2982
3158
  }
2983
3159
  /**
2984
3160
  * Sets the primary sort column to descending order.
@@ -3024,7 +3200,7 @@ var TableQuery = class _TableQuery {
3024
3200
  return new _TableQuery(this.transport, this.tableName, this.database, {
3025
3201
  ...this.state,
3026
3202
  orderByClauses: [...this.state.orderByClauses, orderByClause]
3027
- }, this.getWebSocketTransport);
3203
+ }, this.getWebSocketTransport, this.store);
3028
3204
  }
3029
3205
  /**
3030
3206
  * Sets the maximum number of rows to return.
@@ -3041,7 +3217,7 @@ var TableQuery = class _TableQuery {
3041
3217
  return new _TableQuery(this.transport, this.tableName, this.database, {
3042
3218
  ...this.state,
3043
3219
  limitValue: count
3044
- }, this.getWebSocketTransport);
3220
+ }, this.getWebSocketTransport, this.store);
3045
3221
  }
3046
3222
  /**
3047
3223
  * Sets the number of rows to skip.
@@ -3058,7 +3234,7 @@ var TableQuery = class _TableQuery {
3058
3234
  return new _TableQuery(this.transport, this.tableName, this.database, {
3059
3235
  ...this.state,
3060
3236
  offsetValue: count
3061
- }, this.getWebSocketTransport);
3237
+ }, this.getWebSocketTransport, this.store);
3062
3238
  }
3063
3239
  /**
3064
3240
  * Requests cross-partition access for this query.
@@ -3073,10 +3249,21 @@ var TableQuery = class _TableQuery {
3073
3249
  return new _TableQuery(this.transport, this.tableName, this.database, {
3074
3250
  ...this.state,
3075
3251
  crossPartitionAccess: true
3076
- }, this.getWebSocketTransport);
3252
+ }, this.getWebSocketTransport, this.store);
3077
3253
  }
3078
3254
  /**
3079
- * Restricts the columns returned in the result.
3255
+ * Pin this query at at least this C-1 token. Observes the token into the
3256
+ * client store (I3, sticky) and presents it on execute via `X-Aouda-Token`.
3257
+ */
3258
+ atLeast(token) {
3259
+ if (typeof token !== "string" || token.trim().length === 0) {
3260
+ throw new Error("atLeast() requires a non-empty token");
3261
+ }
3262
+ this.store?.observe(this.database, token);
3263
+ return this.withState({ ...this.state, atLeast: token });
3264
+ }
3265
+ /**
3266
+ * Selects specific columns to return.
3080
3267
  * If not called, all columns are returned.
3081
3268
  *
3082
3269
  * When T is a specific row type, only keys of T are accepted as column names.
@@ -3098,7 +3285,7 @@ var TableQuery = class _TableQuery {
3098
3285
  return new _TableQuery(this.transport, this.tableName, this.database, {
3099
3286
  ...this.state,
3100
3287
  selectColumns: columns.length > 0 ? columns : null
3101
- }, this.getWebSocketTransport);
3288
+ }, this.getWebSocketTransport, this.store);
3102
3289
  }
3103
3290
  /**
3104
3291
  * Return only distinct (de-duplicated) rows for the given columns — SQL `SELECT DISTINCT`.
@@ -3128,7 +3315,7 @@ var TableQuery = class _TableQuery {
3128
3315
  ...this.state,
3129
3316
  selectColumns: columns,
3130
3317
  isDistinct: true
3131
- }, this.getWebSocketTransport);
3318
+ }, this.getWebSocketTransport, this.store);
3132
3319
  }
3133
3320
  /**
3134
3321
  * Adds server-side computed columns to the query result.
@@ -3157,7 +3344,7 @@ var TableQuery = class _TableQuery {
3157
3344
  return new _TableQuery(this.transport, this.tableName, this.database, {
3158
3345
  ...this.state,
3159
3346
  selectExprs: projections
3160
- }, this.getWebSocketTransport);
3347
+ }, this.getWebSocketTransport, this.store);
3161
3348
  }
3162
3349
  join(rightTable, leftColumnOrColumns, rightColumnOrColumns) {
3163
3350
  return this.addJoinClause(
@@ -3215,7 +3402,8 @@ var TableQuery = class _TableQuery {
3215
3402
  }
3216
3403
  ]
3217
3404
  },
3218
- this.getWebSocketTransport
3405
+ this.getWebSocketTransport,
3406
+ this.store
3219
3407
  );
3220
3408
  }
3221
3409
  /**
@@ -3251,7 +3439,8 @@ var TableQuery = class _TableQuery {
3251
3439
  ...this.state,
3252
3440
  groupByColumns: [...columns]
3253
3441
  },
3254
- this.getWebSocketTransport
3442
+ this.getWebSocketTransport,
3443
+ this.store
3255
3444
  );
3256
3445
  }
3257
3446
  /**
@@ -3285,8 +3474,11 @@ var TableQuery = class _TableQuery {
3285
3474
  onSnapshot: options.onSnapshot,
3286
3475
  onChange: options.onChange,
3287
3476
  onError: options.onError,
3288
- conflate: options.conflate
3289
- });
3477
+ conflate: options.conflate,
3478
+ atLeast: options.atLeast ?? this.state.atLeast,
3479
+ waitMs: options.waitMs,
3480
+ onExceeded: options.onExceeded
3481
+ }, void 0, this.store, this.database);
3290
3482
  subscription.start();
3291
3483
  return subscription;
3292
3484
  }
@@ -3401,7 +3593,8 @@ var TableQuery = class _TableQuery {
3401
3593
  ...this.state,
3402
3594
  joinClauses: [...this.state.joinClauses, joinClause]
3403
3595
  },
3404
- this.getWebSocketTransport
3596
+ this.getWebSocketTransport,
3597
+ this.store
3405
3598
  );
3406
3599
  }
3407
3600
  addAggregate(op, column) {
@@ -3422,7 +3615,8 @@ var TableQuery = class _TableQuery {
3422
3615
  }
3423
3616
  ]
3424
3617
  },
3425
- this.getWebSocketTransport
3618
+ this.getWebSocketTransport,
3619
+ this.store
3426
3620
  );
3427
3621
  }
3428
3622
  requireWebSocketTransport() {
@@ -3452,17 +3646,23 @@ var TableQuery = class _TableQuery {
3452
3646
  * ```
3453
3647
  */
3454
3648
  async execute() {
3649
+ if (this.state.atLeast) {
3650
+ this.store?.observe(this.database, this.state.atLeast);
3651
+ }
3455
3652
  const request = this.buildRequest();
3456
3653
  const path4 = `${databasePath2(this.database)}/query`;
3457
3654
  const response = await this.transport.post(path4, request);
3458
3655
  const rows = columnarToRows(response);
3459
3656
  const stats = response.stats;
3460
- return { rows, stats };
3657
+ return { rows, stats, token: response.token };
3461
3658
  }
3462
3659
  /**
3463
3660
  * Executes the query and returns the raw columnar JSON payload (no row-object conversion).
3464
3661
  */
3465
3662
  async toColumnar() {
3663
+ if (this.state.atLeast) {
3664
+ this.store?.observe(this.database, this.state.atLeast);
3665
+ }
3466
3666
  const request = this.buildRequest();
3467
3667
  const path4 = `${databasePath2(this.database)}/query`;
3468
3668
  return this.transport.post(path4, request);
@@ -3492,8 +3692,12 @@ var TableQuery = class _TableQuery {
3492
3692
  limitValue: 0,
3493
3693
  selectColumns: []
3494
3694
  },
3495
- this.getWebSocketTransport
3695
+ this.getWebSocketTransport,
3696
+ this.store
3496
3697
  );
3698
+ if (this.state.atLeast) {
3699
+ this.store?.observe(this.database, this.state.atLeast);
3700
+ }
3497
3701
  const request = countQuery.buildRequest();
3498
3702
  const path4 = `${databasePath2(this.database)}/query`;
3499
3703
  const response = await this.transport.post(path4, request);
@@ -3540,7 +3744,8 @@ var TableQuery = class _TableQuery {
3540
3744
  const response = await this.transport.post(path4, body);
3541
3745
  const result = {
3542
3746
  rowsInserted: response.rowsInserted,
3543
- executionMs: response.executionMs
3747
+ executionMs: response.executionMs,
3748
+ token: response.token
3544
3749
  };
3545
3750
  if (response.generatedValues !== void 0) {
3546
3751
  result.generatedValues = response.generatedValues;
@@ -3590,7 +3795,8 @@ var TableQuery = class _TableQuery {
3590
3795
  const response = await this.transport.post(path4, body);
3591
3796
  const result = {
3592
3797
  rowsInserted: response.rowsInserted,
3593
- executionMs: response.executionMs
3798
+ executionMs: response.executionMs,
3799
+ token: response.token
3594
3800
  };
3595
3801
  if (response.generatedValues !== void 0) {
3596
3802
  result.generatedValues = response.generatedValues;
@@ -3642,6 +3848,7 @@ var TableQuery = class _TableQuery {
3642
3848
  return {
3643
3849
  rowsAffected: response.rowsUpdated,
3644
3850
  executionMs: response.executionMs,
3851
+ token: response.token,
3645
3852
  ...response.rows ? { rows: columnarToRows(response.rows) } : {},
3646
3853
  ...response.rowsTruncated ? { rowsTruncated: true } : {}
3647
3854
  };
@@ -3692,6 +3899,7 @@ var TableQuery = class _TableQuery {
3692
3899
  rowsAffected: response.rowsDeleted,
3693
3900
  executionMs: response.executionMs,
3694
3901
  hasMore: response.hasMore,
3902
+ token: response.token,
3695
3903
  ...response.rows ? { rows: columnarToRows(response.rows) } : {},
3696
3904
  ...response.rowsTruncated ? { rowsTruncated: true } : {}
3697
3905
  };
@@ -3711,7 +3919,8 @@ var TableQuery = class _TableQuery {
3711
3919
  return {
3712
3920
  rowsAffected: response.rowsDeleted,
3713
3921
  executionMs: response.executionMs,
3714
- hasMore: false
3922
+ hasMore: false,
3923
+ token: response.token
3715
3924
  };
3716
3925
  }
3717
3926
  /**
@@ -3724,7 +3933,14 @@ var TableQuery = class _TableQuery {
3724
3933
  throw new Error("batch() requires a non-empty operations array");
3725
3934
  }
3726
3935
  const wireOperations = operations.map((op, index) => {
3727
- const base = new _TableQuery(this.transport, this.tableName, this.database);
3936
+ const base = new _TableQuery(
3937
+ this.transport,
3938
+ this.tableName,
3939
+ this.database,
3940
+ void 0,
3941
+ this.getWebSocketTransport,
3942
+ this.store
3943
+ );
3728
3944
  const scoped = op.where(base);
3729
3945
  const where = scoped.buildWhereClause();
3730
3946
  if (!where) {
@@ -5061,12 +5277,12 @@ function raiseDeprecationWarnings(sink, warnings) {
5061
5277
  continue;
5062
5278
  }
5063
5279
  const sunset = warning.sunsetAt != null ? ` sunsetAt=${warning.sunsetAt}` : "";
5064
- const hash = warning.hash != null && warning.hash.length > 0 ? ` hash=${warning.hash}` : "";
5280
+ const name = warning.name != null && warning.name.length > 0 ? ` name=${warning.name}` : "";
5065
5281
  sink({
5066
5282
  code: warning.code,
5067
- hash: warning.hash,
5283
+ name: warning.name,
5068
5284
  sunsetAt: warning.sunsetAt,
5069
- message: `${warning.code}:${hash}${sunset}`.trim()
5285
+ message: `${warning.code}:${name}${sunset}`.trim()
5070
5286
  });
5071
5287
  }
5072
5288
  }
@@ -5079,24 +5295,28 @@ function emptyStats() {
5079
5295
  };
5080
5296
  }
5081
5297
  var NamedQueriesApi = class {
5082
- constructor(transport, database, onWarning, getStreamingTransport) {
5298
+ constructor(transport, database, onWarning, getStreamingTransport, store) {
5083
5299
  this.transport = transport;
5084
5300
  this.database = database;
5085
5301
  this.onWarning = onWarning;
5086
5302
  this.getStreamingTransport = getStreamingTransport;
5303
+ this.store = store;
5087
5304
  }
5088
- async execute(hash, args, options) {
5089
- if (typeof hash !== "string" || hash.trim().length === 0) {
5090
- throw new Error("Named query hash must be a non-empty string");
5305
+ async execute(name, args, options) {
5306
+ if (typeof name !== "string" || name.trim().length === 0) {
5307
+ throw new Error("Named query name must be a non-empty string");
5091
5308
  }
5092
5309
  const prefix = databasePath2(this.database);
5093
- const path4 = `${prefix}/named-queries/${encodeURIComponent(hash)}/query?format=columnar`;
5310
+ const path4 = `${prefix}/named-queries/${encodeURIComponent(name)}/query?format=columnar`;
5094
5311
  const body = {
5095
5312
  args: args ?? {}
5096
5313
  };
5097
5314
  if (options?.orderByIndex !== void 0) {
5098
5315
  body.orderByIndex = options.orderByIndex;
5099
5316
  }
5317
+ if (options?.atLeast !== void 0) {
5318
+ this.store?.observe(this.database, options.atLeast);
5319
+ }
5100
5320
  const response = await this.transport.post(
5101
5321
  path4,
5102
5322
  body,
@@ -5108,7 +5328,8 @@ var NamedQueriesApi = class {
5108
5328
  rows,
5109
5329
  stats: response.stats,
5110
5330
  warnings: response.warnings,
5111
- totalMatches: response.totalMatches
5331
+ totalMatches: response.totalMatches,
5332
+ token: response.token
5112
5333
  };
5113
5334
  }
5114
5335
  async batch(items, options) {
@@ -5126,6 +5347,9 @@ var NamedQueriesApi = class {
5126
5347
  400
5127
5348
  );
5128
5349
  }
5350
+ if (options?.atLeast !== void 0) {
5351
+ this.store?.observe(this.database, options.atLeast);
5352
+ }
5129
5353
  const prefix = databasePath2(this.database);
5130
5354
  const path4 = `${prefix}/named-queries/batch?format=columnar`;
5131
5355
  const envelope = await this.transport.post(
@@ -5148,34 +5372,41 @@ var NamedQueriesApi = class {
5148
5372
  rowCount: slot.rowCount ?? 0,
5149
5373
  stats: slot.stats ?? emptyStats(),
5150
5374
  warnings: slot.warnings,
5151
- totalMatches: slot.totalMatches
5375
+ totalMatches: slot.totalMatches,
5376
+ token: slot.token
5152
5377
  };
5153
5378
  const result = {
5154
5379
  rows: columnarToRows(columnar),
5155
5380
  stats: columnar.stats,
5156
5381
  warnings: slot.warnings,
5157
- totalMatches: slot.totalMatches
5382
+ totalMatches: slot.totalMatches,
5383
+ token: slot.token
5158
5384
  };
5159
5385
  raiseDeprecationWarnings(this.onWarning, slot.warnings);
5160
5386
  return { isError: false, result };
5161
5387
  });
5162
5388
  }
5163
- subscribe(hash, args, options = {}) {
5164
- if (typeof hash !== "string" || hash.trim().length === 0) {
5165
- throw new Error("Named query hash must be a non-empty string");
5389
+ subscribe(name, args, options = {}) {
5390
+ if (typeof name !== "string" || name.trim().length === 0) {
5391
+ throw new Error("Named query name must be a non-empty string");
5166
5392
  }
5167
5393
  const subscription = new TableSubscription(
5168
5394
  this.getStreamingTransport(),
5169
- { kind: "named", hash, args, orderByIndex: options.orderByIndex },
5395
+ { kind: "named", name, args, orderByIndex: options.orderByIndex },
5170
5396
  {
5171
5397
  onSnapshot: options.onSnapshot,
5172
5398
  onChange: options.onChange,
5173
5399
  onError: options.onError,
5174
- conflate: options.conflate
5400
+ conflate: options.conflate,
5401
+ atLeast: options.atLeast,
5402
+ waitMs: options.waitMs,
5403
+ onExceeded: options.onExceeded
5175
5404
  },
5176
5405
  (warnings) => {
5177
5406
  raiseDeprecationWarnings(this.onWarning, warnings);
5178
- }
5407
+ },
5408
+ this.store,
5409
+ this.database
5179
5410
  );
5180
5411
  subscription.start();
5181
5412
  return subscription;
@@ -5187,12 +5418,12 @@ var NamedMutationsApi = class {
5187
5418
  this.database = database;
5188
5419
  this.onWarning = onWarning;
5189
5420
  }
5190
- async execute(hash, args) {
5191
- if (typeof hash !== "string" || hash.trim().length === 0) {
5192
- throw new Error("Named mutation hash must be a non-empty string");
5421
+ async execute(name, args) {
5422
+ if (typeof name !== "string" || name.trim().length === 0) {
5423
+ throw new Error("Named mutation name must be a non-empty string");
5193
5424
  }
5194
5425
  const prefix = databasePath2(this.database);
5195
- const path4 = `${prefix}/named-mutations/${encodeURIComponent(hash)}/execute`;
5426
+ const path4 = `${prefix}/named-mutations/${encodeURIComponent(name)}/execute`;
5196
5427
  const wire = await this.transport.post(path4, {
5197
5428
  args: args ?? {}
5198
5429
  });
@@ -5253,6 +5484,7 @@ var WebSocketTransport = class {
5253
5484
  // Serialized-send queue: each send appends to this tail.
5254
5485
  this._sendTail = Promise.resolve();
5255
5486
  this._lastVersion = 0;
5487
+ this._lastToken = null;
5256
5488
  this._reconnectAttempt = 0;
5257
5489
  this._disposed = false;
5258
5490
  this._missedPings = 0;
@@ -5270,6 +5502,7 @@ var WebSocketTransport = class {
5270
5502
  this._pingIntervalMs = options.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
5271
5503
  this._maxMissedHeartbeats = options.maxMissedHeartbeats ?? DEFAULT_MAX_MISSED_HEARTBEATS;
5272
5504
  this._enableCompression = options.enableCompression ?? true;
5505
+ this._store = options.consistencyTokenStore;
5273
5506
  this._wireMode = options.wireMode ?? "json";
5274
5507
  this._authHandler?.setOnAccessTokenRefreshed((token) => {
5275
5508
  if (this._handshakeComplete && this._ws?.readyState === WS_READY_STATE_OPEN) {
@@ -5278,10 +5511,14 @@ var WebSocketTransport = class {
5278
5511
  });
5279
5512
  }
5280
5513
  // ─── Public API ────────────────────────────────────────────────────────────
5281
- /** Latest version number from the most recent server `heartbeat` message. */
5514
+ /** Latest change-event sequence from the most recent server `heartbeat` message. */
5282
5515
  get lastVersion() {
5283
5516
  return this._lastVersion;
5284
5517
  }
5518
+ /** Consistency token from the most recent server `heartbeat` (ADR 0042 D-13). */
5519
+ get lastToken() {
5520
+ return this._lastToken;
5521
+ }
5285
5522
  /**
5286
5523
  * Registers a handler for incoming server messages with the given channel id.
5287
5524
  * Use `"__global__"` to receive broadcast messages that carry no `id`.
@@ -5469,6 +5706,10 @@ var WebSocketTransport = class {
5469
5706
  }
5470
5707
  case "heartbeat":
5471
5708
  this._lastVersion = msg.version;
5709
+ this._lastToken = msg.token ?? null;
5710
+ if (this._lastToken != null && this._lastToken.length > 0) {
5711
+ this._store?.observe(this._database, this._lastToken);
5712
+ }
5472
5713
  break;
5473
5714
  case "pong":
5474
5715
  this._missedPings = 0;
@@ -5634,16 +5875,21 @@ var LongPollTransport = class {
5634
5875
  this._connected = false;
5635
5876
  this._pollTask = null;
5636
5877
  this._lastVersion = 0;
5878
+ this._lastToken = null;
5637
5879
  this._serverUrl = serverUrl;
5638
5880
  this._database = database;
5639
5881
  this._authHandler = options.authHandler;
5640
5882
  this._onReconnected = options.onReconnected;
5641
5883
  this._waitMs = Math.max(1, options.waitMs ?? DEFAULT_WAIT_MS);
5642
5884
  this._fetch = options.fetchImpl ?? localNetworkFetch;
5885
+ this._store = options.consistencyTokenStore;
5643
5886
  }
5644
5887
  get lastVersion() {
5645
5888
  return this._lastVersion;
5646
5889
  }
5890
+ get lastToken() {
5891
+ return this._lastToken;
5892
+ }
5647
5893
  registerHandler(id, handler) {
5648
5894
  this._handlers.set(id, handler);
5649
5895
  }
@@ -5766,6 +6012,10 @@ var LongPollTransport = class {
5766
6012
  for (const msg of payload.messages) {
5767
6013
  if (msg.type === "heartbeat") {
5768
6014
  this._lastVersion = msg.version;
6015
+ this._lastToken = msg.token ?? null;
6016
+ if (this._lastToken != null && this._lastToken.length > 0) {
6017
+ this._store?.observe(this._database, this._lastToken);
6018
+ }
5769
6019
  }
5770
6020
  const id = "id" in msg ? msg.id : void 0;
5771
6021
  if (id !== void 0 && id !== null) {
@@ -5845,6 +6095,9 @@ var FallbackStreamingTransport = class {
5845
6095
  get lastVersion() {
5846
6096
  return this._active.lastVersion;
5847
6097
  }
6098
+ get lastToken() {
6099
+ return this._active.lastToken;
6100
+ }
5848
6101
  registerHandler(id, handler) {
5849
6102
  this._handlers.set(id, handler);
5850
6103
  this._active.registerHandler(id, handler);
@@ -5924,6 +6177,7 @@ var AoudaClient = class {
5924
6177
  this.baseUrl = normalizeBaseUrl(options.serverUrl);
5925
6178
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
5926
6179
  this.database = options.database.trim();
6180
+ this._store = options.consistencyTokenStore ?? new MemoryConsistencyTokenStore();
5927
6181
  if (!Number.isFinite(this.timeout) || this.timeout <= 0) {
5928
6182
  throw new Error("timeout must be a finite positive number");
5929
6183
  }
@@ -5958,7 +6212,9 @@ var AoudaClient = class {
5958
6212
  }
5959
6213
  const httpTransport = new HttpTransport({
5960
6214
  baseUrl: this.baseUrl,
5961
- timeout: this.timeout
6215
+ timeout: this.timeout,
6216
+ database: this.database,
6217
+ consistencyTokenStore: this._store
5962
6218
  });
5963
6219
  const activeAuth = options.serverAuth ?? options.appAuth;
5964
6220
  if (activeAuth) {
@@ -6018,7 +6274,8 @@ var AoudaClient = class {
6018
6274
  this.transport,
6019
6275
  this.database,
6020
6276
  onNamedArtifactWarning,
6021
- () => this._getOrCreateWebSocketTransport()
6277
+ () => this._getOrCreateWebSocketTransport(),
6278
+ this._store
6022
6279
  );
6023
6280
  this._namedMutations = new NamedMutationsApi(
6024
6281
  this.transport,
@@ -6117,7 +6374,8 @@ var AoudaClient = class {
6117
6374
  name,
6118
6375
  this.database,
6119
6376
  void 0,
6120
- () => this._getOrCreateWebSocketTransport()
6377
+ () => this._getOrCreateWebSocketTransport(),
6378
+ this._store
6121
6379
  );
6122
6380
  }
6123
6381
  /**
@@ -6169,13 +6427,26 @@ var AoudaClient = class {
6169
6427
  return this._materializedQueries;
6170
6428
  }
6171
6429
  /**
6172
- * Hash-only named-query execute and batch. Names are codegen aliases (D-5).
6430
+ * Named-query execute, read-only batch, and subscribe by unique schema name.
6173
6431
  */
6174
6432
  get namedQueries() {
6175
6433
  return this._namedQueries;
6176
6434
  }
6435
+ observeConsistencyToken(token) {
6436
+ this._store.observe(this.database, token);
6437
+ }
6438
+ getObservedConsistencyToken() {
6439
+ return this._store.get(this.database);
6440
+ }
6441
+ async getConsistencyToken() {
6442
+ const body = await this.transport.get(
6443
+ `/api/databases/${encodeURIComponent(this.database)}/token`
6444
+ );
6445
+ this._store.observe(this.database, body.token);
6446
+ return this._store.get(this.database) ?? body.token;
6447
+ }
6177
6448
  /**
6178
- * Hash-only named-mutation execute. No batch.
6449
+ * Named-mutation execute by unique schema name. No batch.
6179
6450
  */
6180
6451
  get namedMutations() {
6181
6452
  return this._namedMutations;
@@ -6302,13 +6573,15 @@ var AoudaClient = class {
6302
6573
  const primary = new WebSocketTransport(this.baseUrl, this.database, {
6303
6574
  authHandler: this._authHandler,
6304
6575
  enableCompression: this._streamingEnableCompression,
6305
- wireMode: this._streamingWireMode
6576
+ wireMode: this._streamingWireMode,
6577
+ consistencyTokenStore: this._store
6306
6578
  });
6307
6579
  if (this._streamingEnableLongPollFallback) {
6308
6580
  this._wsTransport = new FallbackStreamingTransport(primary, () => {
6309
6581
  return new LongPollTransport(this.baseUrl, this.database, {
6310
6582
  authHandler: this._authHandler,
6311
- waitMs: this._streamingLongPollWaitMs
6583
+ waitMs: this._streamingLongPollWaitMs,
6584
+ consistencyTokenStore: this._store
6312
6585
  });
6313
6586
  });
6314
6587
  } else {
@@ -6333,7 +6606,7 @@ Usage:
6333
6606
  npx @aouda/client schema <command> [options]
6334
6607
 
6335
6608
  Commands:
6336
- generate Fetch schema from Aouda server and output TypeScript types (tables + named query/mutation hashes)
6609
+ generate Fetch schema from Aouda server and output TypeScript types (tables + optional named query/mutation Args/Row)
6337
6610
  schema Schema management (diff, apply, export, validate, history, seed)
6338
6611
  diff Show migration plan (desired vs current)
6339
6612
  apply Apply schema (use --allow-destructive for drops; --dry-run to preview)