@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.cjs CHANGED
@@ -203,14 +203,15 @@ var MessagingError = class extends Error {
203
203
  this.remedy = remedy;
204
204
  if (cause !== void 0) this.cause = cause;
205
205
  }
206
- /** True when retrying after the host re-establishes a session is the fix. */
206
+ /** True when re-running the same call is the fix. */
207
207
  get isRetryable() {
208
- return this.code === "session-unavailable" || this.code === "transport";
208
+ return this.code === "session-unavailable" || this.code === "contention" || this.code === "transport";
209
209
  }
210
210
  };
211
211
  var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
212
212
  var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
213
213
  var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
214
+ var CONTENTION_CODES = /* @__PURE__ */ new Set(["40P01", "40001", "55P03"]);
214
215
  var SESSION_MARKERS = [
215
216
  "auth session missing",
216
217
  "jwt expired",
@@ -232,6 +233,14 @@ function normalizeMessagingError(error, operation) {
232
233
  error
233
234
  );
234
235
  }
236
+ if (code !== void 0 && CONTENTION_CODES.has(code)) {
237
+ return new MessagingError(
238
+ "contention",
239
+ `${operation}: the database resolved a lock collision by cancelling this call (${code}: ${message})`,
240
+ "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.",
241
+ error
242
+ );
243
+ }
235
244
  if (code !== void 0 && FORBIDDEN_CODES.has(code)) {
236
245
  return new MessagingError(
237
246
  "forbidden",
@@ -259,7 +268,7 @@ function normalizeMessagingError(error, operation) {
259
268
  return new MessagingError(
260
269
  "transport",
261
270
  `${operation}: ${message}`,
262
- "Transient transport failure. The package retries reads; a write is surfaced to the outbox so the typed message is never lost.",
271
+ "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).",
263
272
  error
264
273
  );
265
274
  }
@@ -1564,6 +1573,12 @@ function requireOrg(organizationId, operation) {
1564
1573
  }
1565
1574
  return organizationId;
1566
1575
  }
1576
+ var CONTENTION_ATTEMPTS = 3;
1577
+ var CONTENTION_BACKOFF_MS = [60, 180];
1578
+ function sleep(ms) {
1579
+ const jittered = ms + Math.random() * ms * 0.5;
1580
+ return new Promise((resolve) => setTimeout(resolve, jittered));
1581
+ }
1567
1582
  function createMessagingRepository(options) {
1568
1583
  const { identity } = options;
1569
1584
  const org = requireOrg(identity.organizationId, "createMessagingRepository");
@@ -1575,18 +1590,31 @@ function createMessagingRepository(options) {
1575
1590
  const db = () => client.schema(MESSAGING_SCHEMA);
1576
1591
  const rpcDb = () => client.schema(MESSAGING_RPC_SCHEMA);
1577
1592
  async function withSessionRetry(operation, run) {
1578
- try {
1579
- return await run();
1580
- } catch (error) {
1581
- const normalized = normalizeMessagingError(error, operation);
1582
- if (normalized.code !== "session-unavailable" || options.resolveSession === void 0) {
1583
- throw normalized;
1584
- }
1585
- await options.resolveSession();
1593
+ let sessionRetried = false;
1594
+ for (let attempt = 1; ; attempt += 1) {
1586
1595
  try {
1587
- return await run();
1588
- } catch (retryError) {
1589
- throw normalizeMessagingError(retryError, operation);
1596
+ const value = await run();
1597
+ if (attempt > 1) {
1598
+ options.onDiagnostic?.(
1599
+ `${operation}: healed on attempt ${attempt} after a database lock collision (rolled back, safe to re-run).`
1600
+ );
1601
+ }
1602
+ return value;
1603
+ } catch (error) {
1604
+ const normalized = normalizeMessagingError(error, operation);
1605
+ if (normalized.code === "session-unavailable" && options.resolveSession !== void 0 && !sessionRetried) {
1606
+ sessionRetried = true;
1607
+ await options.resolveSession();
1608
+ continue;
1609
+ }
1610
+ if (normalized.code === "contention" && attempt < CONTENTION_ATTEMPTS) {
1611
+ options.onDiagnostic?.(
1612
+ `${operation}: attempt ${attempt}/${CONTENTION_ATTEMPTS} hit a database lock collision (${normalized.message}) \u2014 re-running in ${CONTENTION_BACKOFF_MS[attempt - 1] ?? 0}ms.`
1613
+ );
1614
+ await sleep(CONTENTION_BACKOFF_MS[attempt - 1] ?? 0);
1615
+ continue;
1616
+ }
1617
+ throw normalized;
1590
1618
  }
1591
1619
  }
1592
1620
  }
@@ -1970,6 +1998,10 @@ function MessagingRuntime(props) {
1970
1998
  // contract, which `supabase-shape.test.ts` proves a real client honors.
1971
1999
  client,
1972
2000
  identity,
2001
+ // An automatic heal ANNOUNCES itself: a lock collision the package
2002
+ // re-ran is a warning in the host's diagnostics, never a banner on the
2003
+ // inbox and never silence.
2004
+ onDiagnostic: (message) => report({ level: "warn", message }),
1973
2005
  ...props.resolveSession !== void 0 ? { resolveSession: props.resolveSession } : {}
1974
2006
  });
1975
2007
  let built = null;