@aouda/client 0.1.8 → 0.1.10

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.8",
424
+ version: "0.1.10",
425
425
  description: "Official TypeScript/JavaScript client library for Aouda",
426
426
  type: "module",
427
427
  main: "./dist/index.cjs",
@@ -536,12 +536,13 @@ var AoudaResponseError = class extends AoudaError {
536
536
  }
537
537
  };
538
538
  var AoudaApiError = class extends AoudaError {
539
- constructor(message, code, statusCode, details, requestId) {
539
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds) {
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
+ this.retryAfterSeconds = retryAfterSeconds;
545
546
  this.name = "AoudaApiError";
546
547
  }
547
548
  };
@@ -564,8 +565,8 @@ var AoudaValidationError = class extends AoudaApiError {
564
565
  }
565
566
  };
566
567
  var AoudaServerError = class extends AoudaApiError {
567
- constructor(message, code, statusCode, details, requestId) {
568
- super(message, code, statusCode, details, requestId);
568
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds) {
569
+ super(message, code, statusCode, details, requestId, retryAfterSeconds);
569
570
  this.name = "AoudaServerError";
570
571
  }
571
572
  };
@@ -1504,12 +1505,22 @@ var ERROR_CODE_MAP = {
1504
1505
  SERVICE_UNAVAILABLE: AoudaServerError,
1505
1506
  TIMEOUT: AoudaServerError,
1506
1507
  OVERLOADED: AoudaServerError,
1508
+ MEMORY_BUDGET_EXCEEDED: AoudaServerError,
1509
+ WAL_CAPACITY_EXCEEDED: AoudaServerError,
1507
1510
  AUTH_TOKEN_MISSING: AoudaAuthenticationError,
1508
1511
  AUTH_TOKEN_EXPIRED: AoudaAuthenticationError,
1509
1512
  AUTH_TOKEN_INVALID: AoudaAuthenticationError,
1510
1513
  AUTH_TOKEN_REVOKED: AoudaAuthenticationError,
1511
1514
  AUTH_API_KEY_INVALID: AoudaAuthenticationError,
1512
- AUTH_REQUIRED: AoudaAuthenticationError
1515
+ AUTH_REQUIRED: AoudaAuthenticationError,
1516
+ NAMED_QUERY_NOT_FOUND: AoudaNotFoundError,
1517
+ NAMED_MUTATION_NOT_FOUND: AoudaNotFoundError,
1518
+ NAMED_QUERY_BATCH_EMPTY: AoudaValidationError,
1519
+ NAMED_QUERY_BATCH_TOO_LARGE: AoudaValidationError,
1520
+ NAMED_QUERY_BATCH_MUTATION: AoudaValidationError,
1521
+ NAMED_QUERY_BIND_FAILED: AoudaValidationError,
1522
+ NAMED_QUERY_PARAM_REQUIRED: AoudaValidationError,
1523
+ NAMED_MUTATION_BIND_FAILED: AoudaValidationError
1513
1524
  };
1514
1525
  function createComposedAbortController(...signals) {
1515
1526
  const controller = new AbortController();
@@ -1531,13 +1542,22 @@ function createComposedAbortController(...signals) {
1531
1542
  }
1532
1543
  return controller;
1533
1544
  }
1534
- function createApiError(statusCode, statusText, body) {
1545
+ function parseRetryAfterSeconds(header) {
1546
+ if (header == null || header.trim() === "") return void 0;
1547
+ const trimmed = header.trim();
1548
+ if (!/^\d+$/.test(trimmed)) return void 0;
1549
+ const n = Number.parseInt(trimmed, 10);
1550
+ if (!Number.isFinite(n) || n < 0) return void 0;
1551
+ return n;
1552
+ }
1553
+ function createApiError(statusCode, statusText, body, retryAfterHeader) {
1535
1554
  const message = body.error ?? `${statusCode} ${statusText}`;
1536
1555
  const code = body.code ?? "UNKNOWN";
1537
1556
  const details = body.details;
1538
1557
  const requestId = body.requestId;
1558
+ const retryAfterSeconds = parseRetryAfterSeconds(retryAfterHeader ?? null);
1539
1559
  const Ctor = ERROR_CODE_MAP[code] ?? AoudaApiError;
1540
- return new Ctor(message, code, statusCode, details, requestId);
1560
+ return new Ctor(message, code, statusCode, details, requestId, retryAfterSeconds);
1541
1561
  }
1542
1562
  var HttpTransport = class {
1543
1563
  constructor(options) {
@@ -1630,7 +1650,8 @@ var HttpTransport = class {
1630
1650
  throw createApiError(
1631
1651
  response.status,
1632
1652
  response.statusText,
1633
- errorBody
1653
+ errorBody,
1654
+ response.headers.get("Retry-After")
1634
1655
  );
1635
1656
  }
1636
1657
  throw new AoudaResponseError(
@@ -1740,7 +1761,8 @@ var HttpTransport = class {
1740
1761
  throw createApiError(
1741
1762
  response.status,
1742
1763
  response.statusText,
1743
- errorBody
1764
+ errorBody,
1765
+ response.headers.get("Retry-After")
1744
1766
  );
1745
1767
  }
1746
1768
  throw new AoudaResponseError(
@@ -2086,6 +2108,17 @@ var CIRCUIT_BREAKER_POLICY_DISABLED = {
2086
2108
 
2087
2109
  // src/resilience/resilient-transport.ts
2088
2110
  var REQUEST_ID_HEADER3 = "X-Request-Id";
2111
+ function isCapacityBackPressure(error) {
2112
+ return error instanceof AoudaApiError && (error.code === "MEMORY_BUDGET_EXCEEDED" || error.code === "WAL_CAPACITY_EXCEEDED");
2113
+ }
2114
+ function retryDelayMs(retryPolicy, attempt, error) {
2115
+ const backoff = calculateDelay(retryPolicy, attempt);
2116
+ if (!isCapacityBackPressure(error) || !(error instanceof AoudaApiError)) {
2117
+ return backoff;
2118
+ }
2119
+ const headerMs = error.retryAfterSeconds != null ? error.retryAfterSeconds * 1e3 : 0;
2120
+ return Math.max(backoff, headerMs);
2121
+ }
2089
2122
  function delayMs(ms, signal) {
2090
2123
  if (ms <= 0) return Promise.resolve();
2091
2124
  return new Promise((resolve4, reject) => {
@@ -2140,7 +2173,7 @@ var ResilientTransport = class {
2140
2173
  } catch (error) {
2141
2174
  lastError = error;
2142
2175
  const retryable = isRetryable(error);
2143
- if (retryable) {
2176
+ if (retryable && !isCapacityBackPressure(error)) {
2144
2177
  this.circuitBreaker.recordFailure(error);
2145
2178
  }
2146
2179
  if (!retryable) {
@@ -2150,7 +2183,7 @@ var ResilientTransport = class {
2150
2183
  if (attempt > this.retryPolicy.maxRetries) {
2151
2184
  throw error;
2152
2185
  }
2153
- const delay = calculateDelay(this.retryPolicy, attempt);
2186
+ const delay = retryDelayMs(this.retryPolicy, attempt, error);
2154
2187
  try {
2155
2188
  await delayMs(delay, config.signal);
2156
2189
  } catch {
@@ -2186,7 +2219,7 @@ var ResilientTransport = class {
2186
2219
  } catch (error) {
2187
2220
  lastError = error;
2188
2221
  const retryable = isRetryable(error);
2189
- if (retryable) {
2222
+ if (retryable && !isCapacityBackPressure(error)) {
2190
2223
  this.circuitBreaker.recordFailure(error);
2191
2224
  }
2192
2225
  if (!retryable) {
@@ -2196,7 +2229,7 @@ var ResilientTransport = class {
2196
2229
  if (attempt > this.retryPolicy.maxRetries) {
2197
2230
  throw error;
2198
2231
  }
2199
- const delay = calculateDelay(this.retryPolicy, attempt);
2232
+ const delay = retryDelayMs(this.retryPolicy, attempt, error);
2200
2233
  try {
2201
2234
  await delayMs(delay, config.signal);
2202
2235
  } catch {
@@ -2380,6 +2413,7 @@ var TableSubscription = class {
2380
2413
  this._started = false;
2381
2414
  this._startPromise = null;
2382
2415
  this._lastVersion = 0;
2416
+ this._pendingSnapshotRows = [];
2383
2417
  this.id = createStreamingId("sub");
2384
2418
  this._transport = transport;
2385
2419
  this._tableName = tableName;
@@ -2436,6 +2470,7 @@ var TableSubscription = class {
2436
2470
  return;
2437
2471
  }
2438
2472
  try {
2473
+ this._pendingSnapshotRows = [];
2439
2474
  const resumeFrom = this._lastVersion > 0 ? this._lastVersion : void 0;
2440
2475
  await this._sendSubscribe(resumeFrom);
2441
2476
  } catch (error) {
@@ -2464,7 +2499,13 @@ var TableSubscription = class {
2464
2499
  }
2465
2500
  switch (message.type) {
2466
2501
  case "snapshot":
2467
- this._handleSnapshot(message);
2502
+ this._handleSnapshotPage(message);
2503
+ return;
2504
+ case "snapshot_complete":
2505
+ this._handleSnapshotComplete(message);
2506
+ return;
2507
+ case "gap":
2508
+ void this._handleGap(message);
2468
2509
  return;
2469
2510
  case "change":
2470
2511
  this._handleChange(message);
@@ -2476,9 +2517,13 @@ var TableSubscription = class {
2476
2517
  return;
2477
2518
  }
2478
2519
  }
2479
- _handleSnapshot(message) {
2520
+ _handleSnapshotPage(message) {
2521
+ this._pendingSnapshotRows.push(...message.rows);
2522
+ }
2523
+ _handleSnapshotComplete(message) {
2480
2524
  this._lastVersion = message.version;
2481
- const rows = message.rows;
2525
+ const rows = this._pendingSnapshotRows;
2526
+ this._pendingSnapshotRows = [];
2482
2527
  this._onSnapshot?.(rows, message.version);
2483
2528
  this._queue.push({
2484
2529
  type: "snapshot",
@@ -2486,6 +2531,16 @@ var TableSubscription = class {
2486
2531
  version: message.version
2487
2532
  });
2488
2533
  }
2534
+ async _handleGap(message) {
2535
+ this._pendingSnapshotRows = [];
2536
+ this._lastVersion = message.last_seq;
2537
+ try {
2538
+ await this._sendSubscribe(message.last_seq);
2539
+ } catch (error) {
2540
+ const wrapped = error instanceof Error ? error : new AoudaConnectionError("Subscription gap resume failed");
2541
+ this._notifyError(wrapped);
2542
+ }
2543
+ }
2489
2544
  _handleChange(message) {
2490
2545
  this._lastVersion = message.version;
2491
2546
  const event = {
@@ -2500,6 +2555,7 @@ var TableSubscription = class {
2500
2555
  this._queue.push(event);
2501
2556
  }
2502
2557
  _handleServerError(message) {
2558
+ this._pendingSnapshotRows = [];
2503
2559
  const error = new AoudaConnectionError(
2504
2560
  `Subscription error (${message.code}): ${message.message}`
2505
2561
  );
@@ -4907,6 +4963,147 @@ var MaterializedQueriesApi = class {
4907
4963
  }
4908
4964
  };
4909
4965
 
4966
+ // src/named-queries.ts
4967
+ var MAX_NAMED_QUERY_BATCH_SIZE = 32;
4968
+ function raiseDeprecationWarnings(sink, warnings) {
4969
+ if (warnings == null) return;
4970
+ for (const warning of warnings) {
4971
+ if (warning.code !== "NAMED_QUERY_DEPRECATED" && warning.code !== "NAMED_MUTATION_DEPRECATED") {
4972
+ continue;
4973
+ }
4974
+ const sunset = warning.sunsetAt != null ? ` sunsetAt=${warning.sunsetAt}` : "";
4975
+ const hash = warning.hash != null && warning.hash.length > 0 ? ` hash=${warning.hash}` : "";
4976
+ sink({
4977
+ code: warning.code,
4978
+ hash: warning.hash,
4979
+ sunsetAt: warning.sunsetAt,
4980
+ message: `${warning.code}:${hash}${sunset}`.trim()
4981
+ });
4982
+ }
4983
+ }
4984
+ function emptyStats() {
4985
+ return {
4986
+ rowsScanned: 0,
4987
+ rowsReturned: 0,
4988
+ segmentsAccessed: 0,
4989
+ executionMs: 0
4990
+ };
4991
+ }
4992
+ var NamedQueriesApi = class {
4993
+ constructor(transport, database, onWarning) {
4994
+ this.transport = transport;
4995
+ this.database = database;
4996
+ this.onWarning = onWarning;
4997
+ }
4998
+ async execute(hash, args, options) {
4999
+ if (typeof hash !== "string" || hash.trim().length === 0) {
5000
+ throw new Error("Named query hash must be a non-empty string");
5001
+ }
5002
+ const prefix = databasePath2(this.database);
5003
+ const path4 = `${prefix}/named-queries/${encodeURIComponent(hash)}/query?format=columnar`;
5004
+ const response = await this.transport.post(
5005
+ path4,
5006
+ { args: args ?? {} },
5007
+ { signal: options?.signal }
5008
+ );
5009
+ const rows = columnarToRows(response);
5010
+ raiseDeprecationWarnings(this.onWarning, response.warnings);
5011
+ return {
5012
+ rows,
5013
+ stats: response.stats,
5014
+ warnings: response.warnings
5015
+ };
5016
+ }
5017
+ async batch(items, options) {
5018
+ if (items.length === 0) {
5019
+ throw new AoudaValidationError(
5020
+ "Named query batch requires a non-empty queries array.",
5021
+ "NAMED_QUERY_BATCH_EMPTY",
5022
+ 400
5023
+ );
5024
+ }
5025
+ if (items.length > MAX_NAMED_QUERY_BATCH_SIZE) {
5026
+ throw new AoudaValidationError(
5027
+ `Named query batch exceeds ${MAX_NAMED_QUERY_BATCH_SIZE} elements.`,
5028
+ "NAMED_QUERY_BATCH_TOO_LARGE",
5029
+ 400
5030
+ );
5031
+ }
5032
+ const prefix = databasePath2(this.database);
5033
+ const path4 = `${prefix}/named-queries/batch?format=columnar`;
5034
+ const envelope = await this.transport.post(
5035
+ path4,
5036
+ { queries: items },
5037
+ { signal: options?.signal }
5038
+ );
5039
+ return (envelope.results ?? []).map((slot) => {
5040
+ if (slot.code != null && slot.code.length > 0) {
5041
+ return {
5042
+ isError: true,
5043
+ code: slot.code,
5044
+ error: slot.error
5045
+ };
5046
+ }
5047
+ const columnar = {
5048
+ columns: slot.columns ?? [],
5049
+ types: slot.types ?? [],
5050
+ data: slot.data ?? [],
5051
+ rowCount: slot.rowCount ?? 0,
5052
+ stats: slot.stats ?? emptyStats(),
5053
+ warnings: slot.warnings
5054
+ };
5055
+ const result = {
5056
+ rows: columnarToRows(columnar),
5057
+ stats: columnar.stats,
5058
+ warnings: slot.warnings
5059
+ };
5060
+ raiseDeprecationWarnings(this.onWarning, slot.warnings);
5061
+ return { isError: false, result };
5062
+ });
5063
+ }
5064
+ };
5065
+ var NamedMutationsApi = class {
5066
+ constructor(transport, database, onWarning) {
5067
+ this.transport = transport;
5068
+ this.database = database;
5069
+ this.onWarning = onWarning;
5070
+ }
5071
+ async execute(hash, args) {
5072
+ if (typeof hash !== "string" || hash.trim().length === 0) {
5073
+ throw new Error("Named mutation hash must be a non-empty string");
5074
+ }
5075
+ const prefix = databasePath2(this.database);
5076
+ const path4 = `${prefix}/named-mutations/${encodeURIComponent(hash)}/execute`;
5077
+ const wire = await this.transport.post(path4, {
5078
+ args: args ?? {}
5079
+ });
5080
+ let op = "unknown";
5081
+ let rowsAffected = 0;
5082
+ if (wire.rowsInserted != null) {
5083
+ op = "insert";
5084
+ rowsAffected = wire.rowsInserted;
5085
+ } else if (wire.rowsUpdated != null) {
5086
+ op = "update";
5087
+ rowsAffected = wire.rowsUpdated;
5088
+ } else if (wire.rowsDeleted != null) {
5089
+ op = "delete";
5090
+ rowsAffected = wire.rowsDeleted;
5091
+ }
5092
+ raiseDeprecationWarnings(this.onWarning, wire.warnings);
5093
+ const returning = wire.rows == null ? void 0 : {
5094
+ rows: columnarToRows(wire.rows),
5095
+ stats: wire.rows.stats,
5096
+ warnings: wire.rows.warnings
5097
+ };
5098
+ return {
5099
+ op,
5100
+ rowsAffected,
5101
+ returning,
5102
+ warnings: wire.warnings
5103
+ };
5104
+ }
5105
+ };
5106
+
4910
5107
  // src/streaming/websocket-transport.ts
4911
5108
  var import_msgpack = require("@msgpack/msgpack");
4912
5109
  var DEFAULT_PING_INTERVAL_MS = 2e4;
@@ -5669,6 +5866,19 @@ var AoudaClient = class {
5669
5866
  this.transport,
5670
5867
  this.database
5671
5868
  );
5869
+ const onNamedArtifactWarning = options.onNamedArtifactWarning ?? ((warning) => {
5870
+ console.warn(warning.message);
5871
+ });
5872
+ this._namedQueries = new NamedQueriesApi(
5873
+ this.transport,
5874
+ this.database,
5875
+ onNamedArtifactWarning
5876
+ );
5877
+ this._namedMutations = new NamedMutationsApi(
5878
+ this.transport,
5879
+ this.database,
5880
+ onNamedArtifactWarning
5881
+ );
5672
5882
  }
5673
5883
  /**
5674
5884
  * Connects to the Aouda server.
@@ -5811,6 +6021,18 @@ var AoudaClient = class {
5811
6021
  get materializedQueries() {
5812
6022
  return this._materializedQueries;
5813
6023
  }
6024
+ /**
6025
+ * Hash-only named-query execute and batch. Names are codegen aliases (D-5).
6026
+ */
6027
+ get namedQueries() {
6028
+ return this._namedQueries;
6029
+ }
6030
+ /**
6031
+ * Hash-only named-mutation execute. No batch.
6032
+ */
6033
+ get namedMutations() {
6034
+ return this._namedMutations;
6035
+ }
5814
6036
  /**
5815
6037
  * Access auth operations (signUp, signIn, signOut, refresh, me, changePassword).
5816
6038
  * @returns The auth API.
@@ -5959,7 +6181,7 @@ Usage:
5959
6181
  npx @aouda/client schema <command> [options]
5960
6182
 
5961
6183
  Commands:
5962
- generate Fetch schema from Aouda server and output TypeScript types
6184
+ generate Fetch schema from Aouda server and output TypeScript types (tables + named query/mutation hashes)
5963
6185
  schema Schema management (diff, apply, export, validate, history, seed)
5964
6186
  diff Show migration plan (desired vs current)
5965
6187
  apply Apply schema (use --allow-destructive for drops; --dry-run to preview)