@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.
@@ -421,7 +421,7 @@ var import_node_url = require("url");
421
421
  // package.json
422
422
  var package_default = {
423
423
  name: "@aouda/client",
424
- version: "0.1.14",
424
+ version: "0.1.15",
425
425
  description: "Official TypeScript/JavaScript client library for Aouda",
426
426
  type: "module",
427
427
  main: "./dist/index.cjs",
@@ -536,43 +536,44 @@ var AoudaResponseError = class extends AoudaError {
536
536
  }
537
537
  };
538
538
  var AoudaApiError = class extends AoudaError {
539
- constructor(message, code, statusCode, details, requestId, retryAfterSeconds) {
539
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
540
540
  super(message);
541
541
  this.code = code;
542
542
  this.statusCode = statusCode;
543
543
  this.details = details;
544
544
  this.requestId = requestId;
545
545
  this.retryAfterSeconds = retryAfterSeconds;
546
+ this.token = token;
546
547
  this.name = "AoudaApiError";
547
548
  }
548
549
  };
549
550
  var AoudaNotFoundError = class extends AoudaApiError {
550
- constructor(message, code, statusCode, details, requestId) {
551
- super(message, code, statusCode, details, requestId);
551
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
552
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
552
553
  this.name = "AoudaNotFoundError";
553
554
  }
554
555
  };
555
556
  var AoudaConflictError = class extends AoudaApiError {
556
- constructor(message, code, statusCode, details, requestId) {
557
- super(message, code, statusCode, details, requestId);
557
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
558
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
558
559
  this.name = "AoudaConflictError";
559
560
  }
560
561
  };
561
562
  var AoudaValidationError = class extends AoudaApiError {
562
- constructor(message, code, statusCode, details, requestId) {
563
- super(message, code, statusCode, details, requestId);
563
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
564
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
564
565
  this.name = "AoudaValidationError";
565
566
  }
566
567
  };
567
568
  var AoudaServerError = class extends AoudaApiError {
568
- constructor(message, code, statusCode, details, requestId, retryAfterSeconds) {
569
- super(message, code, statusCode, details, requestId, retryAfterSeconds);
569
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
570
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
570
571
  this.name = "AoudaServerError";
571
572
  }
572
573
  };
573
574
  var AoudaAuthenticationError = class extends AoudaApiError {
574
- constructor(message, code, statusCode, details, requestId) {
575
- super(message, code, statusCode, details, requestId);
575
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
576
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
576
577
  this.name = "AoudaAuthenticationError";
577
578
  }
578
579
  };
@@ -1368,7 +1369,7 @@ function buildHandle(jobId, commitResp, wasResumed, transport, database, signal)
1368
1369
  rowsDurablyCommitted: commitResp.rowsLoaded,
1369
1370
  segmentsCreated: commitResp.segmentsCreated,
1370
1371
  committedAtUtc: commitResp.committedAtUtc,
1371
- walPosition: commitResp.walPosition,
1372
+ token: commitResp.token,
1372
1373
  writeConcernSatisfied: commitResp.writeConcernAchieved ?? "acknowledged",
1373
1374
  writeConcernTimedOut: commitResp.writeConcernTimedOut,
1374
1375
  wasResumed,
@@ -1494,6 +1495,7 @@ var PROTOCOL_VERSION_HEADER = "X-Aouda-Protocol-Version";
1494
1495
  var PROTOCOL_VERSION = "1";
1495
1496
  var CONTENT_TYPE_JSON2 = "application/json";
1496
1497
  var REQUEST_ID_HEADER2 = "X-Request-Id";
1498
+ var TOKEN_HEADER = "X-Aouda-Token";
1497
1499
  var ERROR_CODE_MAP = {
1498
1500
  TABLE_NOT_FOUND: AoudaNotFoundError,
1499
1501
  COLUMN_NOT_FOUND: AoudaNotFoundError,
@@ -1533,7 +1535,12 @@ var ERROR_CODE_MAP = {
1533
1535
  AUTH_IDENTITY_INVALID: AoudaValidationError,
1534
1536
  AUTH_IDENTITY_NOT_FOUND: AoudaValidationError,
1535
1537
  BULK_LOAD_TRANSFORM_INTENT_REQUIRED: AoudaValidationError,
1536
- BULK_LOAD_TRANSFORM_INTENT_CONFLICT: AoudaValidationError
1538
+ BULK_LOAD_TRANSFORM_INTENT_CONFLICT: AoudaValidationError,
1539
+ TOKEN_MALFORMED: AoudaValidationError,
1540
+ TOKEN_FOREIGN_DATABASE: AoudaValidationError,
1541
+ TOKEN_EPOCH_SUPERSEDED: AoudaConflictError,
1542
+ TOKEN_UNSATISFIED: AoudaConflictError,
1543
+ TOKEN_FETCH_PRIMARY: AoudaApiError
1537
1544
  };
1538
1545
  function createComposedAbortController(...signals) {
1539
1546
  const controller = new AbortController();
@@ -1570,7 +1577,7 @@ function createApiError(statusCode, statusText, body, retryAfterHeader) {
1570
1577
  const requestId = body.requestId;
1571
1578
  const retryAfterSeconds = parseRetryAfterSeconds(retryAfterHeader ?? null);
1572
1579
  const Ctor = ERROR_CODE_MAP[code] ?? AoudaApiError;
1573
- return new Ctor(message, code, statusCode, details, requestId, retryAfterSeconds);
1580
+ return new Ctor(message, code, statusCode, details, requestId, retryAfterSeconds, body.token);
1574
1581
  }
1575
1582
  var HttpTransport = class {
1576
1583
  constructor(options) {
@@ -1582,6 +1589,29 @@ var HttpTransport = class {
1582
1589
  ...options.defaultHeaders
1583
1590
  };
1584
1591
  this.abortController = new AbortController();
1592
+ this.database = options.database;
1593
+ this.store = options.consistencyTokenStore;
1594
+ }
1595
+ presentTokenHeaders(path4, headers) {
1596
+ if (this.store == null || this.database == null || isAuthPath(path4)) {
1597
+ return;
1598
+ }
1599
+ const token = this.store.get(this.database);
1600
+ if (token != null && token.length > 0) {
1601
+ headers[TOKEN_HEADER] = token;
1602
+ }
1603
+ }
1604
+ observeResponse(_path, response, bodyText, errorCode) {
1605
+ if (this.store == null || this.database == null) {
1606
+ return;
1607
+ }
1608
+ if (errorCode === "TOKEN_MALFORMED" || errorCode === "TOKEN_FOREIGN_DATABASE") {
1609
+ return;
1610
+ }
1611
+ const header = response.headers.get(TOKEN_HEADER);
1612
+ const bodyToken = readBodyToken(bodyText);
1613
+ const token = header != null && header.trim().length > 0 ? header : bodyToken;
1614
+ this.store.observe(this.database, token);
1585
1615
  }
1586
1616
  /**
1587
1617
  * Abort all in-flight and future requests (e.g. on client disconnect).
@@ -1619,6 +1649,7 @@ var HttpTransport = class {
1619
1649
  ...this.defaultHeaders,
1620
1650
  ...config.headers
1621
1651
  };
1652
+ this.presentTokenHeaders(config.path, headers);
1622
1653
  if (config.body !== void 0 && config.body !== null) {
1623
1654
  headers["Content-Type"] = CONTENT_TYPE_JSON2;
1624
1655
  }
@@ -1638,6 +1669,7 @@ var HttpTransport = class {
1638
1669
  const text = await response.text();
1639
1670
  if (config.allowStatuses?.includes(response.status) && text) {
1640
1671
  try {
1672
+ this.observeResponse(config.path, response, text);
1641
1673
  return JSON.parse(text);
1642
1674
  } catch {
1643
1675
  }
@@ -1651,15 +1683,19 @@ var HttpTransport = class {
1651
1683
  }
1652
1684
  if (response.status === 401) {
1653
1685
  const code = errorBody?.code ?? "AUTH_TOKEN_MISSING";
1686
+ this.observeResponse(config.path, response, text, code);
1654
1687
  throw new AoudaAuthenticationError(
1655
1688
  errorBody?.error ?? "Unauthorized",
1656
1689
  code,
1657
1690
  401,
1658
1691
  errorBody?.details,
1659
- errorBody?.requestId
1692
+ errorBody?.requestId,
1693
+ void 0,
1694
+ errorBody?.token
1660
1695
  );
1661
1696
  }
1662
1697
  if (errorBody?.code != null) {
1698
+ this.observeResponse(config.path, response, text, errorBody.code);
1663
1699
  throw createApiError(
1664
1700
  response.status,
1665
1701
  response.statusText,
@@ -1667,6 +1703,7 @@ var HttpTransport = class {
1667
1703
  response.headers.get("Retry-After")
1668
1704
  );
1669
1705
  }
1706
+ this.observeResponse(config.path, response, text);
1670
1707
  throw new AoudaResponseError(
1671
1708
  errorBody?.error ?? `${response.status} ${response.statusText}`,
1672
1709
  response.status,
@@ -1674,6 +1711,7 @@ var HttpTransport = class {
1674
1711
  );
1675
1712
  }
1676
1713
  const responseText = await response.text();
1714
+ this.observeResponse(config.path, response, responseText);
1677
1715
  if (config.rawText) {
1678
1716
  return responseText;
1679
1717
  }
@@ -1734,6 +1772,7 @@ var HttpTransport = class {
1734
1772
  ...this.defaultHeaders,
1735
1773
  ...config.headers
1736
1774
  };
1775
+ this.presentTokenHeaders(config.path, headers);
1737
1776
  if (config.rawBodyStr === void 0 && config.body !== void 0 && config.body !== null) {
1738
1777
  headers["Content-Type"] = CONTENT_TYPE_JSON2;
1739
1778
  }
@@ -1750,6 +1789,7 @@ var HttpTransport = class {
1750
1789
  try {
1751
1790
  const response = await localNetworkFetch(url, init);
1752
1791
  if (response.ok || config.allowStatuses?.includes(response.status)) {
1792
+ this.observeResponse(config.path, response, void 0);
1753
1793
  return response;
1754
1794
  }
1755
1795
  const text = await response.text();
@@ -1762,15 +1802,19 @@ var HttpTransport = class {
1762
1802
  }
1763
1803
  if (response.status === 401) {
1764
1804
  const code = errorBody?.code ?? "AUTH_TOKEN_MISSING";
1805
+ this.observeResponse(config.path, response, text, code);
1765
1806
  throw new AoudaAuthenticationError(
1766
1807
  errorBody?.error ?? "Unauthorized",
1767
1808
  code,
1768
1809
  401,
1769
1810
  errorBody?.details,
1770
- errorBody?.requestId
1811
+ errorBody?.requestId,
1812
+ void 0,
1813
+ errorBody?.token
1771
1814
  );
1772
1815
  }
1773
1816
  if (errorBody?.code != null) {
1817
+ this.observeResponse(config.path, response, text, errorBody.code);
1774
1818
  throw createApiError(
1775
1819
  response.status,
1776
1820
  response.statusText,
@@ -1778,6 +1822,7 @@ var HttpTransport = class {
1778
1822
  response.headers.get("Retry-After")
1779
1823
  );
1780
1824
  }
1825
+ this.observeResponse(config.path, response, text);
1781
1826
  throw new AoudaResponseError(
1782
1827
  errorBody?.error ?? `${response.status} ${response.statusText}`,
1783
1828
  response.status,
@@ -1932,6 +1977,47 @@ var HttpTransport = class {
1932
1977
  });
1933
1978
  }
1934
1979
  };
1980
+ function isAuthPath(path4) {
1981
+ let p = path4;
1982
+ const q = p.indexOf("?");
1983
+ if (q >= 0) {
1984
+ p = p.slice(0, q);
1985
+ }
1986
+ if (/^https?:\/\//i.test(p)) {
1987
+ try {
1988
+ p = new URL(p).pathname;
1989
+ } catch {
1990
+ }
1991
+ }
1992
+ const lower = p.toLowerCase();
1993
+ if (lower === "/api/auth" || lower.startsWith("/api/auth/")) {
1994
+ return true;
1995
+ }
1996
+ const prefix = "/api/databases/";
1997
+ if (!lower.startsWith(prefix)) {
1998
+ return false;
1999
+ }
2000
+ const rest = lower.slice(prefix.length);
2001
+ const slash = rest.indexOf("/");
2002
+ if (slash < 0) {
2003
+ return false;
2004
+ }
2005
+ const afterDb = rest.slice(slash + 1);
2006
+ return afterDb === "auth" || afterDb.startsWith("auth/");
2007
+ }
2008
+ function readBodyToken(bodyText) {
2009
+ if (bodyText == null || bodyText.trim().length === 0) {
2010
+ return void 0;
2011
+ }
2012
+ try {
2013
+ const parsed = JSON.parse(bodyText);
2014
+ if (parsed !== null && typeof parsed === "object" && "token" in parsed && typeof parsed.token === "string") {
2015
+ return parsed.token;
2016
+ }
2017
+ } catch {
2018
+ }
2019
+ return void 0;
2020
+ }
1935
2021
 
1936
2022
  // src/resilience/retry.ts
1937
2023
  var DEFAULT_MAX_RETRIES = 3;
@@ -2364,6 +2450,48 @@ function databasePath2(db) {
2364
2450
  return `/api/databases/${encodeURIComponent(db)}`;
2365
2451
  }
2366
2452
 
2453
+ // src/consistency-token-store.ts
2454
+ var MemoryConsistencyTokenStore = class {
2455
+ constructor() {
2456
+ this.tokens = /* @__PURE__ */ new Map();
2457
+ }
2458
+ get(database) {
2459
+ if (typeof database !== "string" || database.trim().length === 0) {
2460
+ throw new Error("database must be a non-empty string");
2461
+ }
2462
+ return this.tokens.get(normalizeKey(database));
2463
+ }
2464
+ observe(database, token) {
2465
+ if (typeof database !== "string" || database.trim().length === 0) {
2466
+ throw new Error("database must be a non-empty string");
2467
+ }
2468
+ if (token == null || token.trim().length === 0) {
2469
+ return;
2470
+ }
2471
+ const key = normalizeKey(database);
2472
+ const stored = this.tokens.get(key);
2473
+ if (stored === void 0 || compareOrdinal(token, stored) > 0) {
2474
+ this.tokens.set(key, token);
2475
+ }
2476
+ }
2477
+ };
2478
+ function compareOrdinal(left, right) {
2479
+ if (left === right) return 0;
2480
+ return left < right ? -1 : 1;
2481
+ }
2482
+ function maxToken(left, right) {
2483
+ if (left == null || left.length === 0) {
2484
+ return right == null || right.length === 0 ? void 0 : right;
2485
+ }
2486
+ if (right == null || right.length === 0) {
2487
+ return left;
2488
+ }
2489
+ return compareOrdinal(left, right) >= 0 ? left : right;
2490
+ }
2491
+ function normalizeKey(database) {
2492
+ return database.toLowerCase();
2493
+ }
2494
+
2367
2495
  // src/streaming/subscription.ts
2368
2496
  var AsyncEventQueue = class {
2369
2497
  constructor() {
@@ -2420,7 +2548,7 @@ var AsyncEventQueue = class {
2420
2548
  }
2421
2549
  };
2422
2550
  var TableSubscription = class {
2423
- constructor(transport, identity, options = {}, onWarnings) {
2551
+ constructor(transport, identity, options = {}, onWarnings, store, database) {
2424
2552
  this._queue = new AsyncEventQueue();
2425
2553
  this._active = true;
2426
2554
  this._started = false;
@@ -2436,6 +2564,11 @@ var TableSubscription = class {
2436
2564
  this._onError = options.onError;
2437
2565
  this._onWarnings = onWarnings;
2438
2566
  this._conflate = options.conflate;
2567
+ this._atLeast = options.atLeast;
2568
+ this._waitMs = options.waitMs;
2569
+ this._onExceeded = options.onExceeded;
2570
+ this._store = store;
2571
+ this._database = database;
2439
2572
  this._reconnectHandlerKey = `${this.id}::reconnect`;
2440
2573
  }
2441
2574
  get lastVersion() {
@@ -2500,7 +2633,7 @@ var TableSubscription = class {
2500
2633
  id: this.id
2501
2634
  };
2502
2635
  if (this._identity.kind === "named") {
2503
- message.hash = this._identity.hash;
2636
+ message.name = this._identity.name;
2504
2637
  if (this._identity.args !== void 0) {
2505
2638
  message.args = this._identity.args;
2506
2639
  }
@@ -2519,8 +2652,30 @@ var TableSubscription = class {
2519
2652
  if (this._conflate !== void 0) {
2520
2653
  message.conflate = this._conflate;
2521
2654
  }
2655
+ const pin = this._resolvePin();
2656
+ if (pin !== void 0) {
2657
+ message.at_least = pin;
2658
+ }
2659
+ if (this._waitMs !== void 0) {
2660
+ message.wait_ms = this._waitMs;
2661
+ }
2662
+ if (this._onExceeded !== void 0) {
2663
+ message.on_exceeded = this._onExceeded;
2664
+ }
2522
2665
  await this._transport.send(message);
2523
2666
  }
2667
+ _resolvePin() {
2668
+ if (this._atLeast !== void 0 && this._store != null && this._database != null) {
2669
+ this._store.observe(this._database, this._atLeast);
2670
+ }
2671
+ const stored = this._store != null && this._database != null ? this._store.get(this._database) : void 0;
2672
+ return maxToken(this._atLeast, stored);
2673
+ }
2674
+ _observeToken(token) {
2675
+ if (token !== void 0 && this._store != null && this._database != null) {
2676
+ this._store.observe(this._database, token);
2677
+ }
2678
+ }
2524
2679
  _handleMessage(message) {
2525
2680
  if (!this._active) {
2526
2681
  return;
@@ -2547,6 +2702,7 @@ var TableSubscription = class {
2547
2702
  }
2548
2703
  _handleSnapshotPage(message) {
2549
2704
  this._pendingSnapshotRows.push(...message.rows);
2705
+ this._observeToken(message.token);
2550
2706
  }
2551
2707
  _handleSnapshotComplete(message) {
2552
2708
  this._lastVersion = message.version;
@@ -2564,6 +2720,10 @@ var TableSubscription = class {
2564
2720
  if (message.total_matches !== void 0) {
2565
2721
  snapshot.totalMatches = message.total_matches;
2566
2722
  }
2723
+ if (message.token !== void 0) {
2724
+ snapshot.token = message.token;
2725
+ this._observeToken(message.token);
2726
+ }
2567
2727
  this._queue.push(snapshot);
2568
2728
  }
2569
2729
  async _handleGap(message) {
@@ -2589,6 +2749,10 @@ var TableSubscription = class {
2589
2749
  if (message.values_skipped !== void 0) {
2590
2750
  event.values_skipped = message.values_skipped;
2591
2751
  }
2752
+ if (message.token !== void 0) {
2753
+ event.token = message.token;
2754
+ this._observeToken(message.token);
2755
+ }
2592
2756
  this._onChange?.(event);
2593
2757
  this._queue.push(event);
2594
2758
  }
@@ -2933,11 +3097,12 @@ var TableQuery = class _TableQuery {
2933
3097
  * @param state - Optional initial state (used for immutable chaining).
2934
3098
  * @internal Use `client.table()` to create queries.
2935
3099
  */
2936
- constructor(transport, tableName, database, state, getWebSocketTransport) {
3100
+ constructor(transport, tableName, database, state, getWebSocketTransport, store) {
2937
3101
  this.transport = transport;
2938
3102
  this.tableName = tableName;
2939
3103
  this.database = database;
2940
3104
  this.getWebSocketTransport = getWebSocketTransport;
3105
+ this.store = store;
2941
3106
  this.state = state ?? {
2942
3107
  predicates: [],
2943
3108
  groupClauses: [],
@@ -2953,6 +3118,16 @@ var TableQuery = class _TableQuery {
2953
3118
  isDistinct: false
2954
3119
  };
2955
3120
  }
3121
+ withState(state) {
3122
+ return new _TableQuery(
3123
+ this.transport,
3124
+ this.tableName,
3125
+ this.database,
3126
+ state,
3127
+ this.getWebSocketTransport,
3128
+ this.store
3129
+ );
3130
+ }
2956
3131
  where(column, operator, value) {
2957
3132
  const newPredicates = buildWherePredicates(column, operator, value);
2958
3133
  return new _TableQuery(
@@ -2963,7 +3138,8 @@ var TableQuery = class _TableQuery {
2963
3138
  ...this.state,
2964
3139
  predicates: [...this.state.predicates, ...newPredicates]
2965
3140
  },
2966
- this.getWebSocketTransport
3141
+ this.getWebSocketTransport,
3142
+ this.store
2967
3143
  );
2968
3144
  }
2969
3145
  /**
@@ -2982,7 +3158,7 @@ var TableQuery = class _TableQuery {
2982
3158
  return new _TableQuery(this.transport, this.tableName, this.database, {
2983
3159
  ...this.state,
2984
3160
  groupClauses: [...this.state.groupClauses, sub]
2985
- }, this.getWebSocketTransport);
3161
+ }, this.getWebSocketTransport, this.store);
2986
3162
  }
2987
3163
  /**
2988
3164
  * Sets the primary sort column for the query.
@@ -3007,7 +3183,7 @@ var TableQuery = class _TableQuery {
3007
3183
  return new _TableQuery(this.transport, this.tableName, this.database, {
3008
3184
  ...this.state,
3009
3185
  orderByClauses: [orderByClause]
3010
- }, this.getWebSocketTransport);
3186
+ }, this.getWebSocketTransport, this.store);
3011
3187
  }
3012
3188
  /**
3013
3189
  * Sets the primary sort column to descending order.
@@ -3053,7 +3229,7 @@ var TableQuery = class _TableQuery {
3053
3229
  return new _TableQuery(this.transport, this.tableName, this.database, {
3054
3230
  ...this.state,
3055
3231
  orderByClauses: [...this.state.orderByClauses, orderByClause]
3056
- }, this.getWebSocketTransport);
3232
+ }, this.getWebSocketTransport, this.store);
3057
3233
  }
3058
3234
  /**
3059
3235
  * Sets the maximum number of rows to return.
@@ -3070,7 +3246,7 @@ var TableQuery = class _TableQuery {
3070
3246
  return new _TableQuery(this.transport, this.tableName, this.database, {
3071
3247
  ...this.state,
3072
3248
  limitValue: count
3073
- }, this.getWebSocketTransport);
3249
+ }, this.getWebSocketTransport, this.store);
3074
3250
  }
3075
3251
  /**
3076
3252
  * Sets the number of rows to skip.
@@ -3087,7 +3263,7 @@ var TableQuery = class _TableQuery {
3087
3263
  return new _TableQuery(this.transport, this.tableName, this.database, {
3088
3264
  ...this.state,
3089
3265
  offsetValue: count
3090
- }, this.getWebSocketTransport);
3266
+ }, this.getWebSocketTransport, this.store);
3091
3267
  }
3092
3268
  /**
3093
3269
  * Requests cross-partition access for this query.
@@ -3102,10 +3278,21 @@ var TableQuery = class _TableQuery {
3102
3278
  return new _TableQuery(this.transport, this.tableName, this.database, {
3103
3279
  ...this.state,
3104
3280
  crossPartitionAccess: true
3105
- }, this.getWebSocketTransport);
3281
+ }, this.getWebSocketTransport, this.store);
3106
3282
  }
3107
3283
  /**
3108
- * Restricts the columns returned in the result.
3284
+ * Pin this query at at least this C-1 token. Observes the token into the
3285
+ * client store (I3, sticky) and presents it on execute via `X-Aouda-Token`.
3286
+ */
3287
+ atLeast(token) {
3288
+ if (typeof token !== "string" || token.trim().length === 0) {
3289
+ throw new Error("atLeast() requires a non-empty token");
3290
+ }
3291
+ this.store?.observe(this.database, token);
3292
+ return this.withState({ ...this.state, atLeast: token });
3293
+ }
3294
+ /**
3295
+ * Selects specific columns to return.
3109
3296
  * If not called, all columns are returned.
3110
3297
  *
3111
3298
  * When T is a specific row type, only keys of T are accepted as column names.
@@ -3127,7 +3314,7 @@ var TableQuery = class _TableQuery {
3127
3314
  return new _TableQuery(this.transport, this.tableName, this.database, {
3128
3315
  ...this.state,
3129
3316
  selectColumns: columns.length > 0 ? columns : null
3130
- }, this.getWebSocketTransport);
3317
+ }, this.getWebSocketTransport, this.store);
3131
3318
  }
3132
3319
  /**
3133
3320
  * Return only distinct (de-duplicated) rows for the given columns — SQL `SELECT DISTINCT`.
@@ -3157,7 +3344,7 @@ var TableQuery = class _TableQuery {
3157
3344
  ...this.state,
3158
3345
  selectColumns: columns,
3159
3346
  isDistinct: true
3160
- }, this.getWebSocketTransport);
3347
+ }, this.getWebSocketTransport, this.store);
3161
3348
  }
3162
3349
  /**
3163
3350
  * Adds server-side computed columns to the query result.
@@ -3186,7 +3373,7 @@ var TableQuery = class _TableQuery {
3186
3373
  return new _TableQuery(this.transport, this.tableName, this.database, {
3187
3374
  ...this.state,
3188
3375
  selectExprs: projections
3189
- }, this.getWebSocketTransport);
3376
+ }, this.getWebSocketTransport, this.store);
3190
3377
  }
3191
3378
  join(rightTable, leftColumnOrColumns, rightColumnOrColumns) {
3192
3379
  return this.addJoinClause(
@@ -3244,7 +3431,8 @@ var TableQuery = class _TableQuery {
3244
3431
  }
3245
3432
  ]
3246
3433
  },
3247
- this.getWebSocketTransport
3434
+ this.getWebSocketTransport,
3435
+ this.store
3248
3436
  );
3249
3437
  }
3250
3438
  /**
@@ -3280,7 +3468,8 @@ var TableQuery = class _TableQuery {
3280
3468
  ...this.state,
3281
3469
  groupByColumns: [...columns]
3282
3470
  },
3283
- this.getWebSocketTransport
3471
+ this.getWebSocketTransport,
3472
+ this.store
3284
3473
  );
3285
3474
  }
3286
3475
  /**
@@ -3314,8 +3503,11 @@ var TableQuery = class _TableQuery {
3314
3503
  onSnapshot: options.onSnapshot,
3315
3504
  onChange: options.onChange,
3316
3505
  onError: options.onError,
3317
- conflate: options.conflate
3318
- });
3506
+ conflate: options.conflate,
3507
+ atLeast: options.atLeast ?? this.state.atLeast,
3508
+ waitMs: options.waitMs,
3509
+ onExceeded: options.onExceeded
3510
+ }, void 0, this.store, this.database);
3319
3511
  subscription.start();
3320
3512
  return subscription;
3321
3513
  }
@@ -3430,7 +3622,8 @@ var TableQuery = class _TableQuery {
3430
3622
  ...this.state,
3431
3623
  joinClauses: [...this.state.joinClauses, joinClause]
3432
3624
  },
3433
- this.getWebSocketTransport
3625
+ this.getWebSocketTransport,
3626
+ this.store
3434
3627
  );
3435
3628
  }
3436
3629
  addAggregate(op, column) {
@@ -3451,7 +3644,8 @@ var TableQuery = class _TableQuery {
3451
3644
  }
3452
3645
  ]
3453
3646
  },
3454
- this.getWebSocketTransport
3647
+ this.getWebSocketTransport,
3648
+ this.store
3455
3649
  );
3456
3650
  }
3457
3651
  requireWebSocketTransport() {
@@ -3481,17 +3675,23 @@ var TableQuery = class _TableQuery {
3481
3675
  * ```
3482
3676
  */
3483
3677
  async execute() {
3678
+ if (this.state.atLeast) {
3679
+ this.store?.observe(this.database, this.state.atLeast);
3680
+ }
3484
3681
  const request = this.buildRequest();
3485
3682
  const path4 = `${databasePath2(this.database)}/query`;
3486
3683
  const response = await this.transport.post(path4, request);
3487
3684
  const rows = columnarToRows(response);
3488
3685
  const stats = response.stats;
3489
- return { rows, stats };
3686
+ return { rows, stats, token: response.token };
3490
3687
  }
3491
3688
  /**
3492
3689
  * Executes the query and returns the raw columnar JSON payload (no row-object conversion).
3493
3690
  */
3494
3691
  async toColumnar() {
3692
+ if (this.state.atLeast) {
3693
+ this.store?.observe(this.database, this.state.atLeast);
3694
+ }
3495
3695
  const request = this.buildRequest();
3496
3696
  const path4 = `${databasePath2(this.database)}/query`;
3497
3697
  return this.transport.post(path4, request);
@@ -3521,8 +3721,12 @@ var TableQuery = class _TableQuery {
3521
3721
  limitValue: 0,
3522
3722
  selectColumns: []
3523
3723
  },
3524
- this.getWebSocketTransport
3724
+ this.getWebSocketTransport,
3725
+ this.store
3525
3726
  );
3727
+ if (this.state.atLeast) {
3728
+ this.store?.observe(this.database, this.state.atLeast);
3729
+ }
3526
3730
  const request = countQuery.buildRequest();
3527
3731
  const path4 = `${databasePath2(this.database)}/query`;
3528
3732
  const response = await this.transport.post(path4, request);
@@ -3569,7 +3773,8 @@ var TableQuery = class _TableQuery {
3569
3773
  const response = await this.transport.post(path4, body);
3570
3774
  const result = {
3571
3775
  rowsInserted: response.rowsInserted,
3572
- executionMs: response.executionMs
3776
+ executionMs: response.executionMs,
3777
+ token: response.token
3573
3778
  };
3574
3779
  if (response.generatedValues !== void 0) {
3575
3780
  result.generatedValues = response.generatedValues;
@@ -3619,7 +3824,8 @@ var TableQuery = class _TableQuery {
3619
3824
  const response = await this.transport.post(path4, body);
3620
3825
  const result = {
3621
3826
  rowsInserted: response.rowsInserted,
3622
- executionMs: response.executionMs
3827
+ executionMs: response.executionMs,
3828
+ token: response.token
3623
3829
  };
3624
3830
  if (response.generatedValues !== void 0) {
3625
3831
  result.generatedValues = response.generatedValues;
@@ -3671,6 +3877,7 @@ var TableQuery = class _TableQuery {
3671
3877
  return {
3672
3878
  rowsAffected: response.rowsUpdated,
3673
3879
  executionMs: response.executionMs,
3880
+ token: response.token,
3674
3881
  ...response.rows ? { rows: columnarToRows(response.rows) } : {},
3675
3882
  ...response.rowsTruncated ? { rowsTruncated: true } : {}
3676
3883
  };
@@ -3721,6 +3928,7 @@ var TableQuery = class _TableQuery {
3721
3928
  rowsAffected: response.rowsDeleted,
3722
3929
  executionMs: response.executionMs,
3723
3930
  hasMore: response.hasMore,
3931
+ token: response.token,
3724
3932
  ...response.rows ? { rows: columnarToRows(response.rows) } : {},
3725
3933
  ...response.rowsTruncated ? { rowsTruncated: true } : {}
3726
3934
  };
@@ -3740,7 +3948,8 @@ var TableQuery = class _TableQuery {
3740
3948
  return {
3741
3949
  rowsAffected: response.rowsDeleted,
3742
3950
  executionMs: response.executionMs,
3743
- hasMore: false
3951
+ hasMore: false,
3952
+ token: response.token
3744
3953
  };
3745
3954
  }
3746
3955
  /**
@@ -3753,7 +3962,14 @@ var TableQuery = class _TableQuery {
3753
3962
  throw new Error("batch() requires a non-empty operations array");
3754
3963
  }
3755
3964
  const wireOperations = operations.map((op, index) => {
3756
- const base = new _TableQuery(this.transport, this.tableName, this.database);
3965
+ const base = new _TableQuery(
3966
+ this.transport,
3967
+ this.tableName,
3968
+ this.database,
3969
+ void 0,
3970
+ this.getWebSocketTransport,
3971
+ this.store
3972
+ );
3757
3973
  const scoped = op.where(base);
3758
3974
  const where = scoped.buildWhereClause();
3759
3975
  if (!where) {
@@ -5090,12 +5306,12 @@ function raiseDeprecationWarnings(sink, warnings) {
5090
5306
  continue;
5091
5307
  }
5092
5308
  const sunset = warning.sunsetAt != null ? ` sunsetAt=${warning.sunsetAt}` : "";
5093
- const hash = warning.hash != null && warning.hash.length > 0 ? ` hash=${warning.hash}` : "";
5309
+ const name = warning.name != null && warning.name.length > 0 ? ` name=${warning.name}` : "";
5094
5310
  sink({
5095
5311
  code: warning.code,
5096
- hash: warning.hash,
5312
+ name: warning.name,
5097
5313
  sunsetAt: warning.sunsetAt,
5098
- message: `${warning.code}:${hash}${sunset}`.trim()
5314
+ message: `${warning.code}:${name}${sunset}`.trim()
5099
5315
  });
5100
5316
  }
5101
5317
  }
@@ -5108,24 +5324,28 @@ function emptyStats() {
5108
5324
  };
5109
5325
  }
5110
5326
  var NamedQueriesApi = class {
5111
- constructor(transport, database, onWarning, getStreamingTransport) {
5327
+ constructor(transport, database, onWarning, getStreamingTransport, store) {
5112
5328
  this.transport = transport;
5113
5329
  this.database = database;
5114
5330
  this.onWarning = onWarning;
5115
5331
  this.getStreamingTransport = getStreamingTransport;
5332
+ this.store = store;
5116
5333
  }
5117
- async execute(hash, args, options) {
5118
- if (typeof hash !== "string" || hash.trim().length === 0) {
5119
- throw new Error("Named query hash must be a non-empty string");
5334
+ async execute(name, args, options) {
5335
+ if (typeof name !== "string" || name.trim().length === 0) {
5336
+ throw new Error("Named query name must be a non-empty string");
5120
5337
  }
5121
5338
  const prefix = databasePath2(this.database);
5122
- const path4 = `${prefix}/named-queries/${encodeURIComponent(hash)}/query?format=columnar`;
5339
+ const path4 = `${prefix}/named-queries/${encodeURIComponent(name)}/query?format=columnar`;
5123
5340
  const body = {
5124
5341
  args: args ?? {}
5125
5342
  };
5126
5343
  if (options?.orderByIndex !== void 0) {
5127
5344
  body.orderByIndex = options.orderByIndex;
5128
5345
  }
5346
+ if (options?.atLeast !== void 0) {
5347
+ this.store?.observe(this.database, options.atLeast);
5348
+ }
5129
5349
  const response = await this.transport.post(
5130
5350
  path4,
5131
5351
  body,
@@ -5137,7 +5357,8 @@ var NamedQueriesApi = class {
5137
5357
  rows,
5138
5358
  stats: response.stats,
5139
5359
  warnings: response.warnings,
5140
- totalMatches: response.totalMatches
5360
+ totalMatches: response.totalMatches,
5361
+ token: response.token
5141
5362
  };
5142
5363
  }
5143
5364
  async batch(items, options) {
@@ -5155,6 +5376,9 @@ var NamedQueriesApi = class {
5155
5376
  400
5156
5377
  );
5157
5378
  }
5379
+ if (options?.atLeast !== void 0) {
5380
+ this.store?.observe(this.database, options.atLeast);
5381
+ }
5158
5382
  const prefix = databasePath2(this.database);
5159
5383
  const path4 = `${prefix}/named-queries/batch?format=columnar`;
5160
5384
  const envelope = await this.transport.post(
@@ -5177,34 +5401,41 @@ var NamedQueriesApi = class {
5177
5401
  rowCount: slot.rowCount ?? 0,
5178
5402
  stats: slot.stats ?? emptyStats(),
5179
5403
  warnings: slot.warnings,
5180
- totalMatches: slot.totalMatches
5404
+ totalMatches: slot.totalMatches,
5405
+ token: slot.token
5181
5406
  };
5182
5407
  const result = {
5183
5408
  rows: columnarToRows(columnar),
5184
5409
  stats: columnar.stats,
5185
5410
  warnings: slot.warnings,
5186
- totalMatches: slot.totalMatches
5411
+ totalMatches: slot.totalMatches,
5412
+ token: slot.token
5187
5413
  };
5188
5414
  raiseDeprecationWarnings(this.onWarning, slot.warnings);
5189
5415
  return { isError: false, result };
5190
5416
  });
5191
5417
  }
5192
- subscribe(hash, args, options = {}) {
5193
- if (typeof hash !== "string" || hash.trim().length === 0) {
5194
- throw new Error("Named query hash must be a non-empty string");
5418
+ subscribe(name, args, options = {}) {
5419
+ if (typeof name !== "string" || name.trim().length === 0) {
5420
+ throw new Error("Named query name must be a non-empty string");
5195
5421
  }
5196
5422
  const subscription = new TableSubscription(
5197
5423
  this.getStreamingTransport(),
5198
- { kind: "named", hash, args, orderByIndex: options.orderByIndex },
5424
+ { kind: "named", name, args, orderByIndex: options.orderByIndex },
5199
5425
  {
5200
5426
  onSnapshot: options.onSnapshot,
5201
5427
  onChange: options.onChange,
5202
5428
  onError: options.onError,
5203
- conflate: options.conflate
5429
+ conflate: options.conflate,
5430
+ atLeast: options.atLeast,
5431
+ waitMs: options.waitMs,
5432
+ onExceeded: options.onExceeded
5204
5433
  },
5205
5434
  (warnings) => {
5206
5435
  raiseDeprecationWarnings(this.onWarning, warnings);
5207
- }
5436
+ },
5437
+ this.store,
5438
+ this.database
5208
5439
  );
5209
5440
  subscription.start();
5210
5441
  return subscription;
@@ -5216,12 +5447,12 @@ var NamedMutationsApi = class {
5216
5447
  this.database = database;
5217
5448
  this.onWarning = onWarning;
5218
5449
  }
5219
- async execute(hash, args) {
5220
- if (typeof hash !== "string" || hash.trim().length === 0) {
5221
- throw new Error("Named mutation hash must be a non-empty string");
5450
+ async execute(name, args) {
5451
+ if (typeof name !== "string" || name.trim().length === 0) {
5452
+ throw new Error("Named mutation name must be a non-empty string");
5222
5453
  }
5223
5454
  const prefix = databasePath2(this.database);
5224
- const path4 = `${prefix}/named-mutations/${encodeURIComponent(hash)}/execute`;
5455
+ const path4 = `${prefix}/named-mutations/${encodeURIComponent(name)}/execute`;
5225
5456
  const wire = await this.transport.post(path4, {
5226
5457
  args: args ?? {}
5227
5458
  });
@@ -5282,6 +5513,7 @@ var WebSocketTransport = class {
5282
5513
  // Serialized-send queue: each send appends to this tail.
5283
5514
  this._sendTail = Promise.resolve();
5284
5515
  this._lastVersion = 0;
5516
+ this._lastToken = null;
5285
5517
  this._reconnectAttempt = 0;
5286
5518
  this._disposed = false;
5287
5519
  this._missedPings = 0;
@@ -5299,6 +5531,7 @@ var WebSocketTransport = class {
5299
5531
  this._pingIntervalMs = options.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
5300
5532
  this._maxMissedHeartbeats = options.maxMissedHeartbeats ?? DEFAULT_MAX_MISSED_HEARTBEATS;
5301
5533
  this._enableCompression = options.enableCompression ?? true;
5534
+ this._store = options.consistencyTokenStore;
5302
5535
  this._wireMode = options.wireMode ?? "json";
5303
5536
  this._authHandler?.setOnAccessTokenRefreshed((token) => {
5304
5537
  if (this._handshakeComplete && this._ws?.readyState === WS_READY_STATE_OPEN) {
@@ -5307,10 +5540,14 @@ var WebSocketTransport = class {
5307
5540
  });
5308
5541
  }
5309
5542
  // ─── Public API ────────────────────────────────────────────────────────────
5310
- /** Latest version number from the most recent server `heartbeat` message. */
5543
+ /** Latest change-event sequence from the most recent server `heartbeat` message. */
5311
5544
  get lastVersion() {
5312
5545
  return this._lastVersion;
5313
5546
  }
5547
+ /** Consistency token from the most recent server `heartbeat` (ADR 0042 D-13). */
5548
+ get lastToken() {
5549
+ return this._lastToken;
5550
+ }
5314
5551
  /**
5315
5552
  * Registers a handler for incoming server messages with the given channel id.
5316
5553
  * Use `"__global__"` to receive broadcast messages that carry no `id`.
@@ -5498,6 +5735,10 @@ var WebSocketTransport = class {
5498
5735
  }
5499
5736
  case "heartbeat":
5500
5737
  this._lastVersion = msg.version;
5738
+ this._lastToken = msg.token ?? null;
5739
+ if (this._lastToken != null && this._lastToken.length > 0) {
5740
+ this._store?.observe(this._database, this._lastToken);
5741
+ }
5501
5742
  break;
5502
5743
  case "pong":
5503
5744
  this._missedPings = 0;
@@ -5663,16 +5904,21 @@ var LongPollTransport = class {
5663
5904
  this._connected = false;
5664
5905
  this._pollTask = null;
5665
5906
  this._lastVersion = 0;
5907
+ this._lastToken = null;
5666
5908
  this._serverUrl = serverUrl;
5667
5909
  this._database = database;
5668
5910
  this._authHandler = options.authHandler;
5669
5911
  this._onReconnected = options.onReconnected;
5670
5912
  this._waitMs = Math.max(1, options.waitMs ?? DEFAULT_WAIT_MS);
5671
5913
  this._fetch = options.fetchImpl ?? localNetworkFetch;
5914
+ this._store = options.consistencyTokenStore;
5672
5915
  }
5673
5916
  get lastVersion() {
5674
5917
  return this._lastVersion;
5675
5918
  }
5919
+ get lastToken() {
5920
+ return this._lastToken;
5921
+ }
5676
5922
  registerHandler(id, handler) {
5677
5923
  this._handlers.set(id, handler);
5678
5924
  }
@@ -5795,6 +6041,10 @@ var LongPollTransport = class {
5795
6041
  for (const msg of payload.messages) {
5796
6042
  if (msg.type === "heartbeat") {
5797
6043
  this._lastVersion = msg.version;
6044
+ this._lastToken = msg.token ?? null;
6045
+ if (this._lastToken != null && this._lastToken.length > 0) {
6046
+ this._store?.observe(this._database, this._lastToken);
6047
+ }
5798
6048
  }
5799
6049
  const id = "id" in msg ? msg.id : void 0;
5800
6050
  if (id !== void 0 && id !== null) {
@@ -5874,6 +6124,9 @@ var FallbackStreamingTransport = class {
5874
6124
  get lastVersion() {
5875
6125
  return this._active.lastVersion;
5876
6126
  }
6127
+ get lastToken() {
6128
+ return this._active.lastToken;
6129
+ }
5877
6130
  registerHandler(id, handler) {
5878
6131
  this._handlers.set(id, handler);
5879
6132
  this._active.registerHandler(id, handler);
@@ -5953,6 +6206,7 @@ var AoudaClient = class {
5953
6206
  this.baseUrl = normalizeBaseUrl(options.serverUrl);
5954
6207
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
5955
6208
  this.database = options.database.trim();
6209
+ this._store = options.consistencyTokenStore ?? new MemoryConsistencyTokenStore();
5956
6210
  if (!Number.isFinite(this.timeout) || this.timeout <= 0) {
5957
6211
  throw new Error("timeout must be a finite positive number");
5958
6212
  }
@@ -5987,7 +6241,9 @@ var AoudaClient = class {
5987
6241
  }
5988
6242
  const httpTransport = new HttpTransport({
5989
6243
  baseUrl: this.baseUrl,
5990
- timeout: this.timeout
6244
+ timeout: this.timeout,
6245
+ database: this.database,
6246
+ consistencyTokenStore: this._store
5991
6247
  });
5992
6248
  const activeAuth = options.serverAuth ?? options.appAuth;
5993
6249
  if (activeAuth) {
@@ -6047,7 +6303,8 @@ var AoudaClient = class {
6047
6303
  this.transport,
6048
6304
  this.database,
6049
6305
  onNamedArtifactWarning,
6050
- () => this._getOrCreateWebSocketTransport()
6306
+ () => this._getOrCreateWebSocketTransport(),
6307
+ this._store
6051
6308
  );
6052
6309
  this._namedMutations = new NamedMutationsApi(
6053
6310
  this.transport,
@@ -6146,7 +6403,8 @@ var AoudaClient = class {
6146
6403
  name,
6147
6404
  this.database,
6148
6405
  void 0,
6149
- () => this._getOrCreateWebSocketTransport()
6406
+ () => this._getOrCreateWebSocketTransport(),
6407
+ this._store
6150
6408
  );
6151
6409
  }
6152
6410
  /**
@@ -6198,13 +6456,26 @@ var AoudaClient = class {
6198
6456
  return this._materializedQueries;
6199
6457
  }
6200
6458
  /**
6201
- * Hash-only named-query execute and batch. Names are codegen aliases (D-5).
6459
+ * Named-query execute, read-only batch, and subscribe by unique schema name.
6202
6460
  */
6203
6461
  get namedQueries() {
6204
6462
  return this._namedQueries;
6205
6463
  }
6464
+ observeConsistencyToken(token) {
6465
+ this._store.observe(this.database, token);
6466
+ }
6467
+ getObservedConsistencyToken() {
6468
+ return this._store.get(this.database);
6469
+ }
6470
+ async getConsistencyToken() {
6471
+ const body = await this.transport.get(
6472
+ `/api/databases/${encodeURIComponent(this.database)}/token`
6473
+ );
6474
+ this._store.observe(this.database, body.token);
6475
+ return this._store.get(this.database) ?? body.token;
6476
+ }
6206
6477
  /**
6207
- * Hash-only named-mutation execute. No batch.
6478
+ * Named-mutation execute by unique schema name. No batch.
6208
6479
  */
6209
6480
  get namedMutations() {
6210
6481
  return this._namedMutations;
@@ -6331,13 +6602,15 @@ var AoudaClient = class {
6331
6602
  const primary = new WebSocketTransport(this.baseUrl, this.database, {
6332
6603
  authHandler: this._authHandler,
6333
6604
  enableCompression: this._streamingEnableCompression,
6334
- wireMode: this._streamingWireMode
6605
+ wireMode: this._streamingWireMode,
6606
+ consistencyTokenStore: this._store
6335
6607
  });
6336
6608
  if (this._streamingEnableLongPollFallback) {
6337
6609
  this._wsTransport = new FallbackStreamingTransport(primary, () => {
6338
6610
  return new LongPollTransport(this.baseUrl, this.database, {
6339
6611
  authHandler: this._authHandler,
6340
- waitMs: this._streamingLongPollWaitMs
6612
+ waitMs: this._streamingLongPollWaitMs,
6613
+ consistencyTokenStore: this._store
6341
6614
  });
6342
6615
  });
6343
6616
  } else {
@@ -6363,7 +6636,7 @@ Usage:
6363
6636
  npx @aouda/client schema <command> [options]
6364
6637
 
6365
6638
  Commands:
6366
- generate Fetch schema from Aouda server and output TypeScript types (tables + named query/mutation hashes)
6639
+ generate Fetch schema from Aouda server and output TypeScript types (tables + optional named query/mutation Args/Row)
6367
6640
  schema Schema management (diff, apply, export, validate, history, seed)
6368
6641
  diff Show migration plan (desired vs current)
6369
6642
  apply Apply schema (use --allow-destructive for drops; --dry-run to preview)