@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/CHANGELOG.md +45 -0
- package/dist/index.cjs +42 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +42 -14
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +46 -14
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +8 -2
- package/dist/react.d.ts +8 -2
- package/dist/react.js +46 -14
- package/dist/react.js.map +1 -1
- package/dist/styles.css +9 -0
- package/package.json +3 -3
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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
|
}
|