@ai-matrx/messaging 0.11.1 → 0.11.3

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/index.d.cts CHANGED
@@ -794,6 +794,12 @@ interface RepositoryOptions {
794
794
  identity: MessagingIdentity;
795
795
  /** Called once before a retry when a read fails with a missing session. */
796
796
  resolveSession?: SessionResolver | undefined;
797
+ /**
798
+ * Announced automatic interventions. Nothing fails silently and nothing heals
799
+ * silently either: every lock-collision retry below reports here (the React
800
+ * provider wires it to the host's diagnostics as a warning).
801
+ */
802
+ onDiagnostic?: ((message: string) => void) | undefined;
797
803
  /** Profile-lookup cache TTL. Default 5 minutes (the frontend's proven value). */
798
804
  userTtlMs?: number | undefined;
799
805
  now?: (() => number) | undefined;
@@ -1077,14 +1083,14 @@ declare function messagingClientId(): string;
1077
1083
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
1078
1084
  * warns and retries; it is never a red error.
1079
1085
  */
1080
- type MessagingErrorCode = "session-unavailable" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
1086
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
1081
1087
  declare class MessagingError extends Error {
1082
1088
  readonly code: MessagingErrorCode;
1083
1089
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
1084
1090
  readonly remedy: string;
1085
1091
  readonly cause?: unknown;
1086
1092
  constructor(code: MessagingErrorCode, message: string, remedy: string, cause?: unknown);
1087
- /** True when retrying after the host re-establishes a session is the fix. */
1093
+ /** True when re-running the same call is the fix. */
1088
1094
  get isRetryable(): boolean;
1089
1095
  }
1090
1096
  interface PostgrestErrorLike {
package/dist/index.d.ts CHANGED
@@ -794,6 +794,12 @@ interface RepositoryOptions {
794
794
  identity: MessagingIdentity;
795
795
  /** Called once before a retry when a read fails with a missing session. */
796
796
  resolveSession?: SessionResolver | undefined;
797
+ /**
798
+ * Announced automatic interventions. Nothing fails silently and nothing heals
799
+ * silently either: every lock-collision retry below reports here (the React
800
+ * provider wires it to the host's diagnostics as a warning).
801
+ */
802
+ onDiagnostic?: ((message: string) => void) | undefined;
797
803
  /** Profile-lookup cache TTL. Default 5 minutes (the frontend's proven value). */
798
804
  userTtlMs?: number | undefined;
799
805
  now?: (() => number) | undefined;
@@ -1077,14 +1083,14 @@ declare function messagingClientId(): string;
1077
1083
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
1078
1084
  * warns and retries; it is never a red error.
1079
1085
  */
1080
- type MessagingErrorCode = "session-unavailable" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
1086
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
1081
1087
  declare class MessagingError extends Error {
1082
1088
  readonly code: MessagingErrorCode;
1083
1089
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
1084
1090
  readonly remedy: string;
1085
1091
  readonly cause?: unknown;
1086
1092
  constructor(code: MessagingErrorCode, message: string, remedy: string, cause?: unknown);
1087
- /** True when retrying after the host re-establishes a session is the fix. */
1093
+ /** True when re-running the same call is the fix. */
1088
1094
  get isRetryable(): boolean;
1089
1095
  }
1090
1096
  interface PostgrestErrorLike {
package/dist/index.js CHANGED
@@ -159,14 +159,15 @@ var MessagingError = class extends Error {
159
159
  this.remedy = remedy;
160
160
  if (cause !== void 0) this.cause = cause;
161
161
  }
162
- /** True when retrying after the host re-establishes a session is the fix. */
162
+ /** True when re-running the same call is the fix. */
163
163
  get isRetryable() {
164
- return this.code === "session-unavailable" || this.code === "transport";
164
+ return this.code === "session-unavailable" || this.code === "contention" || this.code === "transport";
165
165
  }
166
166
  };
167
167
  var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
168
168
  var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
169
169
  var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
170
+ var CONTENTION_CODES = /* @__PURE__ */ new Set(["40P01", "40001", "55P03"]);
170
171
  var SESSION_MARKERS = [
171
172
  "auth session missing",
172
173
  "jwt expired",
@@ -188,6 +189,14 @@ function normalizeMessagingError(error, operation) {
188
189
  error
189
190
  );
190
191
  }
192
+ if (code !== void 0 && CONTENTION_CODES.has(code)) {
193
+ return new MessagingError(
194
+ "contention",
195
+ `${operation}: the database resolved a lock collision by cancelling this call (${code}: ${message})`,
196
+ "Nothing of this call committed, so it is safe to re-run \u2014 the package re-runs it a bounded number of times and announces each heal. Seeing it repeatedly means real contention on the messaging tables (usually DDL applied against live traffic); look at what else held a lock at that moment, never at the caller.",
197
+ error
198
+ );
199
+ }
191
200
  if (code !== void 0 && FORBIDDEN_CODES.has(code)) {
192
201
  return new MessagingError(
193
202
  "forbidden",
@@ -215,7 +224,7 @@ function normalizeMessagingError(error, operation) {
215
224
  return new MessagingError(
216
225
  "transport",
217
226
  `${operation}: ${message}`,
218
- "Transient transport failure. The package retries reads; a write is surfaced to the outbox so the typed message is never lost.",
227
+ "Transient transport failure. A typed message is surfaced to the outbox so it is never lost; a read is the caller's to re-issue (only `contention` and `session-unavailable` are re-run inside the package).",
219
228
  error
220
229
  );
221
230
  }
@@ -1613,6 +1622,12 @@ function requireOrg(organizationId, operation) {
1613
1622
  }
1614
1623
  return organizationId;
1615
1624
  }
1625
+ var CONTENTION_ATTEMPTS = 3;
1626
+ var CONTENTION_BACKOFF_MS = [60, 180];
1627
+ function sleep(ms) {
1628
+ const jittered = ms + Math.random() * ms * 0.5;
1629
+ return new Promise((resolve) => setTimeout(resolve, jittered));
1630
+ }
1616
1631
  function createMessagingRepository(options) {
1617
1632
  const { identity } = options;
1618
1633
  const org = requireOrg(identity.organizationId, "createMessagingRepository");
@@ -1624,18 +1639,31 @@ function createMessagingRepository(options) {
1624
1639
  const db = () => client.schema(MESSAGING_SCHEMA);
1625
1640
  const rpcDb = () => client.schema(MESSAGING_RPC_SCHEMA);
1626
1641
  async function withSessionRetry(operation, run) {
1627
- try {
1628
- return await run();
1629
- } catch (error) {
1630
- const normalized = normalizeMessagingError(error, operation);
1631
- if (normalized.code !== "session-unavailable" || options.resolveSession === void 0) {
1632
- throw normalized;
1633
- }
1634
- await options.resolveSession();
1642
+ let sessionRetried = false;
1643
+ for (let attempt = 1; ; attempt += 1) {
1635
1644
  try {
1636
- return await run();
1637
- } catch (retryError) {
1638
- throw normalizeMessagingError(retryError, operation);
1645
+ const value = await run();
1646
+ if (attempt > 1) {
1647
+ options.onDiagnostic?.(
1648
+ `${operation}: healed on attempt ${attempt} after a database lock collision (rolled back, safe to re-run).`
1649
+ );
1650
+ }
1651
+ return value;
1652
+ } catch (error) {
1653
+ const normalized = normalizeMessagingError(error, operation);
1654
+ if (normalized.code === "session-unavailable" && options.resolveSession !== void 0 && !sessionRetried) {
1655
+ sessionRetried = true;
1656
+ await options.resolveSession();
1657
+ continue;
1658
+ }
1659
+ if (normalized.code === "contention" && attempt < CONTENTION_ATTEMPTS) {
1660
+ options.onDiagnostic?.(
1661
+ `${operation}: attempt ${attempt}/${CONTENTION_ATTEMPTS} hit a database lock collision (${normalized.message}) \u2014 re-running in ${CONTENTION_BACKOFF_MS[attempt - 1] ?? 0}ms.`
1662
+ );
1663
+ await sleep(CONTENTION_BACKOFF_MS[attempt - 1] ?? 0);
1664
+ continue;
1665
+ }
1666
+ throw normalized;
1639
1667
  }
1640
1668
  }
1641
1669
  }