@aouda/client 0.1.9 → 0.1.11

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.9",
424
+ version: "0.1.11",
425
425
  description: "Official TypeScript/JavaScript client library for Aouda",
426
426
  type: "module",
427
427
  main: "./dist/index.cjs",
@@ -659,6 +659,7 @@ var AuthHandler = class {
659
659
  this._tokenExpiry = null;
660
660
  this._authenticated = false;
661
661
  this._refreshPromise = null;
662
+ this._onAccessTokenRefreshed = null;
662
663
  this._serverUrl = serverUrl.replace(/\/+$/, "");
663
664
  this._timeout = timeout;
664
665
  this._authBasePath = config.basePath;
@@ -908,10 +909,16 @@ var AuthHandler = class {
908
909
  const thresholdSeconds = this._refreshThresholdMs / 1e3;
909
910
  return this._tokenExpiry <= nowSeconds + thresholdSeconds;
910
911
  }
912
+ setOnAccessTokenRefreshed(handler) {
913
+ this._onAccessTokenRefreshed = handler;
914
+ }
911
915
  async _doRefresh() {
912
916
  try {
913
917
  const result = await this.callRefreshEndpoint();
914
918
  this.handleSignInResult(result);
919
+ if (result.accessToken) {
920
+ this._onAccessTokenRefreshed?.(result.accessToken);
921
+ }
915
922
  return true;
916
923
  } catch {
917
924
  return false;
@@ -1512,7 +1519,15 @@ var ERROR_CODE_MAP = {
1512
1519
  AUTH_TOKEN_INVALID: AoudaAuthenticationError,
1513
1520
  AUTH_TOKEN_REVOKED: AoudaAuthenticationError,
1514
1521
  AUTH_API_KEY_INVALID: AoudaAuthenticationError,
1515
- AUTH_REQUIRED: AoudaAuthenticationError
1522
+ AUTH_REQUIRED: AoudaAuthenticationError,
1523
+ NAMED_QUERY_NOT_FOUND: AoudaNotFoundError,
1524
+ NAMED_MUTATION_NOT_FOUND: AoudaNotFoundError,
1525
+ NAMED_QUERY_BATCH_EMPTY: AoudaValidationError,
1526
+ NAMED_QUERY_BATCH_TOO_LARGE: AoudaValidationError,
1527
+ NAMED_QUERY_BATCH_MUTATION: AoudaValidationError,
1528
+ NAMED_QUERY_BIND_FAILED: AoudaValidationError,
1529
+ NAMED_QUERY_PARAM_REQUIRED: AoudaValidationError,
1530
+ NAMED_MUTATION_BIND_FAILED: AoudaValidationError
1516
1531
  };
1517
1532
  function createComposedAbortController(...signals) {
1518
1533
  const controller = new AbortController();
@@ -2405,6 +2420,8 @@ var TableSubscription = class {
2405
2420
  this._started = false;
2406
2421
  this._startPromise = null;
2407
2422
  this._lastVersion = 0;
2423
+ this._pendingSnapshotRows = [];
2424
+ this._forceFreshSubscribe = false;
2408
2425
  this.id = createStreamingId("sub");
2409
2426
  this._transport = transport;
2410
2427
  this._tableName = tableName;
@@ -2412,6 +2429,7 @@ var TableSubscription = class {
2412
2429
  this._onSnapshot = options.onSnapshot;
2413
2430
  this._onChange = options.onChange;
2414
2431
  this._onError = options.onError;
2432
+ this._conflate = options.conflate;
2415
2433
  this._reconnectHandlerKey = `${this.id}::reconnect`;
2416
2434
  }
2417
2435
  get lastVersion() {
@@ -2461,7 +2479,9 @@ var TableSubscription = class {
2461
2479
  return;
2462
2480
  }
2463
2481
  try {
2464
- const resumeFrom = this._lastVersion > 0 ? this._lastVersion : void 0;
2482
+ this._pendingSnapshotRows = [];
2483
+ const resumeFrom = this._forceFreshSubscribe || this._lastVersion <= 0 ? void 0 : this._lastVersion;
2484
+ this._forceFreshSubscribe = false;
2465
2485
  await this._sendSubscribe(resumeFrom);
2466
2486
  } catch (error) {
2467
2487
  const wrapped = error instanceof Error ? error : new AoudaConnectionError("Subscription reconnect failed");
@@ -2481,6 +2501,9 @@ var TableSubscription = class {
2481
2501
  if (resumeFrom !== void 0) {
2482
2502
  message.resume_from = resumeFrom;
2483
2503
  }
2504
+ if (this._conflate !== void 0) {
2505
+ message.conflate = this._conflate;
2506
+ }
2484
2507
  await this._transport.send(message);
2485
2508
  }
2486
2509
  _handleMessage(message) {
@@ -2489,7 +2512,13 @@ var TableSubscription = class {
2489
2512
  }
2490
2513
  switch (message.type) {
2491
2514
  case "snapshot":
2492
- this._handleSnapshot(message);
2515
+ this._handleSnapshotPage(message);
2516
+ return;
2517
+ case "snapshot_complete":
2518
+ this._handleSnapshotComplete(message);
2519
+ return;
2520
+ case "gap":
2521
+ void this._handleGap(message);
2493
2522
  return;
2494
2523
  case "change":
2495
2524
  this._handleChange(message);
@@ -2501,9 +2530,13 @@ var TableSubscription = class {
2501
2530
  return;
2502
2531
  }
2503
2532
  }
2504
- _handleSnapshot(message) {
2533
+ _handleSnapshotPage(message) {
2534
+ this._pendingSnapshotRows.push(...message.rows);
2535
+ }
2536
+ _handleSnapshotComplete(message) {
2505
2537
  this._lastVersion = message.version;
2506
- const rows = message.rows;
2538
+ const rows = this._pendingSnapshotRows;
2539
+ this._pendingSnapshotRows = [];
2507
2540
  this._onSnapshot?.(rows, message.version);
2508
2541
  this._queue.push({
2509
2542
  type: "snapshot",
@@ -2511,6 +2544,16 @@ var TableSubscription = class {
2511
2544
  version: message.version
2512
2545
  });
2513
2546
  }
2547
+ async _handleGap(message) {
2548
+ this._pendingSnapshotRows = [];
2549
+ this._lastVersion = message.last_seq;
2550
+ try {
2551
+ await this._sendSubscribe(message.last_seq);
2552
+ } catch (error) {
2553
+ const wrapped = error instanceof Error ? error : new AoudaConnectionError("Subscription gap resume failed");
2554
+ this._notifyError(wrapped);
2555
+ }
2556
+ }
2514
2557
  _handleChange(message) {
2515
2558
  this._lastVersion = message.version;
2516
2559
  const event = {
@@ -2521,10 +2564,20 @@ var TableSubscription = class {
2521
2564
  key: message.key,
2522
2565
  version: message.version
2523
2566
  };
2567
+ if (message.values_skipped !== void 0) {
2568
+ event.values_skipped = message.values_skipped;
2569
+ }
2524
2570
  this._onChange?.(event);
2525
2571
  this._queue.push(event);
2526
2572
  }
2527
2573
  _handleServerError(message) {
2574
+ if (message.code === "SLOW_CONSUMER") {
2575
+ this._pendingSnapshotRows = [];
2576
+ this._lastVersion = 0;
2577
+ this._forceFreshSubscribe = true;
2578
+ return;
2579
+ }
2580
+ this._pendingSnapshotRows = [];
2528
2581
  const error = new AoudaConnectionError(
2529
2582
  `Subscription error (${message.code}): ${message.message}`
2530
2583
  );
@@ -3237,7 +3290,8 @@ var TableQuery = class _TableQuery {
3237
3290
  {
3238
3291
  onSnapshot: options.onSnapshot,
3239
3292
  onChange: options.onChange,
3240
- onError: options.onError
3293
+ onError: options.onError,
3294
+ conflate: options.conflate
3241
3295
  },
3242
3296
  mergedFilter
3243
3297
  );
@@ -4932,6 +4986,147 @@ var MaterializedQueriesApi = class {
4932
4986
  }
4933
4987
  };
4934
4988
 
4989
+ // src/named-queries.ts
4990
+ var MAX_NAMED_QUERY_BATCH_SIZE = 32;
4991
+ function raiseDeprecationWarnings(sink, warnings) {
4992
+ if (warnings == null) return;
4993
+ for (const warning of warnings) {
4994
+ if (warning.code !== "NAMED_QUERY_DEPRECATED" && warning.code !== "NAMED_MUTATION_DEPRECATED") {
4995
+ continue;
4996
+ }
4997
+ const sunset = warning.sunsetAt != null ? ` sunsetAt=${warning.sunsetAt}` : "";
4998
+ const hash = warning.hash != null && warning.hash.length > 0 ? ` hash=${warning.hash}` : "";
4999
+ sink({
5000
+ code: warning.code,
5001
+ hash: warning.hash,
5002
+ sunsetAt: warning.sunsetAt,
5003
+ message: `${warning.code}:${hash}${sunset}`.trim()
5004
+ });
5005
+ }
5006
+ }
5007
+ function emptyStats() {
5008
+ return {
5009
+ rowsScanned: 0,
5010
+ rowsReturned: 0,
5011
+ segmentsAccessed: 0,
5012
+ executionMs: 0
5013
+ };
5014
+ }
5015
+ var NamedQueriesApi = class {
5016
+ constructor(transport, database, onWarning) {
5017
+ this.transport = transport;
5018
+ this.database = database;
5019
+ this.onWarning = onWarning;
5020
+ }
5021
+ async execute(hash, args, options) {
5022
+ if (typeof hash !== "string" || hash.trim().length === 0) {
5023
+ throw new Error("Named query hash must be a non-empty string");
5024
+ }
5025
+ const prefix = databasePath2(this.database);
5026
+ const path4 = `${prefix}/named-queries/${encodeURIComponent(hash)}/query?format=columnar`;
5027
+ const response = await this.transport.post(
5028
+ path4,
5029
+ { args: args ?? {} },
5030
+ { signal: options?.signal }
5031
+ );
5032
+ const rows = columnarToRows(response);
5033
+ raiseDeprecationWarnings(this.onWarning, response.warnings);
5034
+ return {
5035
+ rows,
5036
+ stats: response.stats,
5037
+ warnings: response.warnings
5038
+ };
5039
+ }
5040
+ async batch(items, options) {
5041
+ if (items.length === 0) {
5042
+ throw new AoudaValidationError(
5043
+ "Named query batch requires a non-empty queries array.",
5044
+ "NAMED_QUERY_BATCH_EMPTY",
5045
+ 400
5046
+ );
5047
+ }
5048
+ if (items.length > MAX_NAMED_QUERY_BATCH_SIZE) {
5049
+ throw new AoudaValidationError(
5050
+ `Named query batch exceeds ${MAX_NAMED_QUERY_BATCH_SIZE} elements.`,
5051
+ "NAMED_QUERY_BATCH_TOO_LARGE",
5052
+ 400
5053
+ );
5054
+ }
5055
+ const prefix = databasePath2(this.database);
5056
+ const path4 = `${prefix}/named-queries/batch?format=columnar`;
5057
+ const envelope = await this.transport.post(
5058
+ path4,
5059
+ { queries: items },
5060
+ { signal: options?.signal }
5061
+ );
5062
+ return (envelope.results ?? []).map((slot) => {
5063
+ if (slot.code != null && slot.code.length > 0) {
5064
+ return {
5065
+ isError: true,
5066
+ code: slot.code,
5067
+ error: slot.error
5068
+ };
5069
+ }
5070
+ const columnar = {
5071
+ columns: slot.columns ?? [],
5072
+ types: slot.types ?? [],
5073
+ data: slot.data ?? [],
5074
+ rowCount: slot.rowCount ?? 0,
5075
+ stats: slot.stats ?? emptyStats(),
5076
+ warnings: slot.warnings
5077
+ };
5078
+ const result = {
5079
+ rows: columnarToRows(columnar),
5080
+ stats: columnar.stats,
5081
+ warnings: slot.warnings
5082
+ };
5083
+ raiseDeprecationWarnings(this.onWarning, slot.warnings);
5084
+ return { isError: false, result };
5085
+ });
5086
+ }
5087
+ };
5088
+ var NamedMutationsApi = class {
5089
+ constructor(transport, database, onWarning) {
5090
+ this.transport = transport;
5091
+ this.database = database;
5092
+ this.onWarning = onWarning;
5093
+ }
5094
+ async execute(hash, args) {
5095
+ if (typeof hash !== "string" || hash.trim().length === 0) {
5096
+ throw new Error("Named mutation hash must be a non-empty string");
5097
+ }
5098
+ const prefix = databasePath2(this.database);
5099
+ const path4 = `${prefix}/named-mutations/${encodeURIComponent(hash)}/execute`;
5100
+ const wire = await this.transport.post(path4, {
5101
+ args: args ?? {}
5102
+ });
5103
+ let op = "unknown";
5104
+ let rowsAffected = 0;
5105
+ if (wire.rowsInserted != null) {
5106
+ op = "insert";
5107
+ rowsAffected = wire.rowsInserted;
5108
+ } else if (wire.rowsUpdated != null) {
5109
+ op = "update";
5110
+ rowsAffected = wire.rowsUpdated;
5111
+ } else if (wire.rowsDeleted != null) {
5112
+ op = "delete";
5113
+ rowsAffected = wire.rowsDeleted;
5114
+ }
5115
+ raiseDeprecationWarnings(this.onWarning, wire.warnings);
5116
+ const returning = wire.rows == null ? void 0 : {
5117
+ rows: columnarToRows(wire.rows),
5118
+ stats: wire.rows.stats,
5119
+ warnings: wire.rows.warnings
5120
+ };
5121
+ return {
5122
+ op,
5123
+ rowsAffected,
5124
+ returning,
5125
+ warnings: wire.warnings
5126
+ };
5127
+ }
5128
+ };
5129
+
4935
5130
  // src/streaming/websocket-transport.ts
4936
5131
  var import_msgpack = require("@msgpack/msgpack");
4937
5132
  var DEFAULT_PING_INTERVAL_MS = 2e4;
@@ -4956,6 +5151,7 @@ var WebSocketTransport = class {
4956
5151
  // Resolved/rejected when the auth handshake completes.
4957
5152
  this._authResolve = null;
4958
5153
  this._authReject = null;
5154
+ this._handshakeComplete = false;
4959
5155
  this._serverUrl = serverUrl;
4960
5156
  this._database = database;
4961
5157
  this._authHandler = options.authHandler;
@@ -4965,6 +5161,11 @@ var WebSocketTransport = class {
4965
5161
  this._maxMissedHeartbeats = options.maxMissedHeartbeats ?? DEFAULT_MAX_MISSED_HEARTBEATS;
4966
5162
  this._enableCompression = options.enableCompression ?? true;
4967
5163
  this._wireMode = options.wireMode ?? "json";
5164
+ this._authHandler?.setOnAccessTokenRefreshed((token) => {
5165
+ if (this._handshakeComplete && this._ws?.readyState === WS_READY_STATE_OPEN) {
5166
+ void this.send({ type: "re_auth", token });
5167
+ }
5168
+ });
4968
5169
  }
4969
5170
  // ─── Public API ────────────────────────────────────────────────────────────
4970
5171
  /** Latest version number from the most recent server `heartbeat` message. */
@@ -5137,6 +5338,7 @@ var WebSocketTransport = class {
5137
5338
  const resolve4 = this._authResolve;
5138
5339
  this._authResolve = null;
5139
5340
  this._authReject = null;
5341
+ this._handshakeComplete = true;
5140
5342
  if (resolve4) resolve4();
5141
5343
  break;
5142
5344
  }
@@ -5163,10 +5365,15 @@ var WebSocketTransport = class {
5163
5365
  break;
5164
5366
  }
5165
5367
  const id = "id" in msg ? msg.id : void 0;
5166
- if (id !== void 0 && id !== null) {
5368
+ if (id !== void 0 && id !== null && id !== "") {
5167
5369
  this._handlers.get(id)?.(msg);
5168
5370
  } else {
5169
5371
  this._handlers.get("__global__")?.(msg);
5372
+ for (const [key, handler] of this._handlers) {
5373
+ if (key !== "__global__") {
5374
+ handler(msg);
5375
+ }
5376
+ }
5170
5377
  }
5171
5378
  }
5172
5379
  // ─── Internal: reconnect ──────────────────────────────────────────────────
@@ -5694,6 +5901,19 @@ var AoudaClient = class {
5694
5901
  this.transport,
5695
5902
  this.database
5696
5903
  );
5904
+ const onNamedArtifactWarning = options.onNamedArtifactWarning ?? ((warning) => {
5905
+ console.warn(warning.message);
5906
+ });
5907
+ this._namedQueries = new NamedQueriesApi(
5908
+ this.transport,
5909
+ this.database,
5910
+ onNamedArtifactWarning
5911
+ );
5912
+ this._namedMutations = new NamedMutationsApi(
5913
+ this.transport,
5914
+ this.database,
5915
+ onNamedArtifactWarning
5916
+ );
5697
5917
  }
5698
5918
  /**
5699
5919
  * Connects to the Aouda server.
@@ -5836,6 +6056,18 @@ var AoudaClient = class {
5836
6056
  get materializedQueries() {
5837
6057
  return this._materializedQueries;
5838
6058
  }
6059
+ /**
6060
+ * Hash-only named-query execute and batch. Names are codegen aliases (D-5).
6061
+ */
6062
+ get namedQueries() {
6063
+ return this._namedQueries;
6064
+ }
6065
+ /**
6066
+ * Hash-only named-mutation execute. No batch.
6067
+ */
6068
+ get namedMutations() {
6069
+ return this._namedMutations;
6070
+ }
5839
6071
  /**
5840
6072
  * Access auth operations (signUp, signIn, signOut, refresh, me, changePassword).
5841
6073
  * @returns The auth API.
@@ -5984,7 +6216,7 @@ Usage:
5984
6216
  npx @aouda/client schema <command> [options]
5985
6217
 
5986
6218
  Commands:
5987
- generate Fetch schema from Aouda server and output TypeScript types
6219
+ generate Fetch schema from Aouda server and output TypeScript types (tables + named query/mutation hashes)
5988
6220
  schema Schema management (diff, apply, export, validate, history, seed)
5989
6221
  diff Show migration plan (desired vs current)
5990
6222
  apply Apply schema (use --allow-destructive for drops; --dry-run to preview)