@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/react.d.cts CHANGED
@@ -691,6 +691,12 @@ interface RepositoryOptions {
691
691
  identity: MessagingIdentity;
692
692
  /** Called once before a retry when a read fails with a missing session. */
693
693
  resolveSession?: SessionResolver | undefined;
694
+ /**
695
+ * Announced automatic interventions. Nothing fails silently and nothing heals
696
+ * silently either: every lock-collision retry below reports here (the React
697
+ * provider wires it to the host's diagnostics as a warning).
698
+ */
699
+ onDiagnostic?: ((message: string) => void) | undefined;
694
700
  /** Profile-lookup cache TTL. Default 5 minutes (the frontend's proven value). */
695
701
  userTtlMs?: number | undefined;
696
702
  now?: (() => number) | undefined;
@@ -1521,14 +1527,14 @@ declare const MESSAGING_EVENTS: {
1521
1527
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
1522
1528
  * warns and retries; it is never a red error.
1523
1529
  */
1524
- type MessagingErrorCode = "session-unavailable" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
1530
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
1525
1531
  declare class MessagingError extends Error {
1526
1532
  readonly code: MessagingErrorCode;
1527
1533
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
1528
1534
  readonly remedy: string;
1529
1535
  readonly cause?: unknown;
1530
1536
  constructor(code: MessagingErrorCode, message: string, remedy: string, cause?: unknown);
1531
- /** True when retrying after the host re-establishes a session is the fix. */
1537
+ /** True when re-running the same call is the fix. */
1532
1538
  get isRetryable(): boolean;
1533
1539
  }
1534
1540
  interface PostgrestErrorLike {
package/dist/react.d.ts CHANGED
@@ -691,6 +691,12 @@ interface RepositoryOptions {
691
691
  identity: MessagingIdentity;
692
692
  /** Called once before a retry when a read fails with a missing session. */
693
693
  resolveSession?: SessionResolver | undefined;
694
+ /**
695
+ * Announced automatic interventions. Nothing fails silently and nothing heals
696
+ * silently either: every lock-collision retry below reports here (the React
697
+ * provider wires it to the host's diagnostics as a warning).
698
+ */
699
+ onDiagnostic?: ((message: string) => void) | undefined;
694
700
  /** Profile-lookup cache TTL. Default 5 minutes (the frontend's proven value). */
695
701
  userTtlMs?: number | undefined;
696
702
  now?: (() => number) | undefined;
@@ -1521,14 +1527,14 @@ declare const MESSAGING_EVENTS: {
1521
1527
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
1522
1528
  * warns and retries; it is never a red error.
1523
1529
  */
1524
- type MessagingErrorCode = "session-unavailable" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
1530
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
1525
1531
  declare class MessagingError extends Error {
1526
1532
  readonly code: MessagingErrorCode;
1527
1533
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
1528
1534
  readonly remedy: string;
1529
1535
  readonly cause?: unknown;
1530
1536
  constructor(code: MessagingErrorCode, message: string, remedy: string, cause?: unknown);
1531
- /** True when retrying after the host re-establishes a session is the fix. */
1537
+ /** True when re-running the same call is the fix. */
1532
1538
  get isRetryable(): boolean;
1533
1539
  }
1534
1540
  interface PostgrestErrorLike {
package/dist/react.js CHANGED
@@ -106,14 +106,15 @@ var MessagingError = class extends Error {
106
106
  this.remedy = remedy;
107
107
  if (cause !== void 0) this.cause = cause;
108
108
  }
109
- /** True when retrying after the host re-establishes a session is the fix. */
109
+ /** True when re-running the same call is the fix. */
110
110
  get isRetryable() {
111
- return this.code === "session-unavailable" || this.code === "transport";
111
+ return this.code === "session-unavailable" || this.code === "contention" || this.code === "transport";
112
112
  }
113
113
  };
114
114
  var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
115
115
  var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
116
116
  var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
117
+ var CONTENTION_CODES = /* @__PURE__ */ new Set(["40P01", "40001", "55P03"]);
117
118
  var SESSION_MARKERS = [
118
119
  "auth session missing",
119
120
  "jwt expired",
@@ -135,6 +136,14 @@ function normalizeMessagingError(error, operation) {
135
136
  error
136
137
  );
137
138
  }
139
+ if (code !== void 0 && CONTENTION_CODES.has(code)) {
140
+ return new MessagingError(
141
+ "contention",
142
+ `${operation}: the database resolved a lock collision by cancelling this call (${code}: ${message})`,
143
+ "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.",
144
+ error
145
+ );
146
+ }
138
147
  if (code !== void 0 && FORBIDDEN_CODES.has(code)) {
139
148
  return new MessagingError(
140
149
  "forbidden",
@@ -162,7 +171,7 @@ function normalizeMessagingError(error, operation) {
162
171
  return new MessagingError(
163
172
  "transport",
164
173
  `${operation}: ${message}`,
165
- "Transient transport failure. The package retries reads; a write is surfaced to the outbox so the typed message is never lost.",
174
+ "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).",
166
175
  error
167
176
  );
168
177
  }
@@ -1469,6 +1478,12 @@ function requireOrg(organizationId, operation) {
1469
1478
  }
1470
1479
  return organizationId;
1471
1480
  }
1481
+ var CONTENTION_ATTEMPTS = 3;
1482
+ var CONTENTION_BACKOFF_MS = [60, 180];
1483
+ function sleep(ms) {
1484
+ const jittered = ms + Math.random() * ms * 0.5;
1485
+ return new Promise((resolve) => setTimeout(resolve, jittered));
1486
+ }
1472
1487
  function createMessagingRepository(options) {
1473
1488
  const { identity } = options;
1474
1489
  const org = requireOrg(identity.organizationId, "createMessagingRepository");
@@ -1480,18 +1495,31 @@ function createMessagingRepository(options) {
1480
1495
  const db = () => client.schema(MESSAGING_SCHEMA);
1481
1496
  const rpcDb = () => client.schema(MESSAGING_RPC_SCHEMA);
1482
1497
  async function withSessionRetry(operation, run) {
1483
- try {
1484
- return await run();
1485
- } catch (error) {
1486
- const normalized = normalizeMessagingError(error, operation);
1487
- if (normalized.code !== "session-unavailable" || options.resolveSession === void 0) {
1488
- throw normalized;
1489
- }
1490
- await options.resolveSession();
1498
+ let sessionRetried = false;
1499
+ for (let attempt = 1; ; attempt += 1) {
1491
1500
  try {
1492
- return await run();
1493
- } catch (retryError) {
1494
- throw normalizeMessagingError(retryError, operation);
1501
+ const value = await run();
1502
+ if (attempt > 1) {
1503
+ options.onDiagnostic?.(
1504
+ `${operation}: healed on attempt ${attempt} after a database lock collision (rolled back, safe to re-run).`
1505
+ );
1506
+ }
1507
+ return value;
1508
+ } catch (error) {
1509
+ const normalized = normalizeMessagingError(error, operation);
1510
+ if (normalized.code === "session-unavailable" && options.resolveSession !== void 0 && !sessionRetried) {
1511
+ sessionRetried = true;
1512
+ await options.resolveSession();
1513
+ continue;
1514
+ }
1515
+ if (normalized.code === "contention" && attempt < CONTENTION_ATTEMPTS) {
1516
+ options.onDiagnostic?.(
1517
+ `${operation}: attempt ${attempt}/${CONTENTION_ATTEMPTS} hit a database lock collision (${normalized.message}) \u2014 re-running in ${CONTENTION_BACKOFF_MS[attempt - 1] ?? 0}ms.`
1518
+ );
1519
+ await sleep(CONTENTION_BACKOFF_MS[attempt - 1] ?? 0);
1520
+ continue;
1521
+ }
1522
+ throw normalized;
1495
1523
  }
1496
1524
  }
1497
1525
  }
@@ -1875,6 +1903,10 @@ function MessagingRuntime(props) {
1875
1903
  // contract, which `supabase-shape.test.ts` proves a real client honors.
1876
1904
  client,
1877
1905
  identity,
1906
+ // An automatic heal ANNOUNCES itself: a lock collision the package
1907
+ // re-ran is a warning in the host's diagnostics, never a banner on the
1908
+ // inbox and never silence.
1909
+ onDiagnostic: (message) => report({ level: "warn", message }),
1878
1910
  ...props.resolveSession !== void 0 ? { resolveSession: props.resolveSession } : {}
1879
1911
  });
1880
1912
  let built = null;