@ai-matrx/messaging 0.13.1 → 0.15.0

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
@@ -207,7 +207,11 @@ interface Page<TItem, TCursor> {
207
207
  /** What the host injects. Identity ONLY — every hard part is in the package. */
208
208
  interface MessagingIdentity {
209
209
  readonly userId: UserId;
210
- readonly organizationId: OrganizationId;
210
+ /**
211
+ * The active organization, or `null` when none is selected. Reads never
212
+ * depend on it; it is only where a NEW conversation lands.
213
+ */
214
+ readonly organizationId: OrganizationId | null;
211
215
  }
212
216
  interface DraftMessage {
213
217
  readonly conversationId: ConversationId;
@@ -628,7 +632,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
628
632
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
629
633
  * warns and retries; it is never a red error.
630
634
  */
631
- type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
635
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
636
+ /** A NEW conversation needs an organization to land in and none is selected. */
637
+ | "organization-required" | "unknown";
632
638
  declare class MessagingError extends Error {
633
639
  readonly code: MessagingErrorCode;
634
640
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
@@ -920,15 +926,21 @@ interface RepositoryOptions {
920
926
  */
921
927
  client: MessagingSupabaseClient;
922
928
  /**
923
- * The active organization. Every conversation and message write carries it
924
- * explicitly (R5).
929
+ * The active organization, or `null`/omitted when none is selected.
930
+ *
931
+ * 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
932
+ * read here is answered by RLS as the session's user; the organization is
933
+ * only the DESTINATION of a NEW conversation. Without one, starting a
934
+ * conversation is refused with `organization-required` (the host asks the
935
+ * person to choose), and a message or member added to an EXISTING
936
+ * conversation is stamped with that conversation's own organization.
925
937
  *
926
938
  * 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
927
939
  * acting is read from `client`'s own session — see `session-identity.ts` for
928
940
  * the class that closed and why a handed-in id could not be made safe by
929
941
  * convention. A caller that still passes `identity` is IGNORED and told so.
930
942
  */
931
- organizationId: string;
943
+ organizationId?: string | null | undefined;
932
944
  /**
933
945
  * INTERNAL. The React provider already runs one session reader for its own
934
946
  * render loop and hands it here so the two cannot answer differently and so
@@ -949,8 +961,11 @@ interface RepositoryOptions {
949
961
  now?: (() => number) | undefined;
950
962
  }
951
963
  interface MessagingRepository {
952
- /** The organization every write here is stamped with. */
953
- readonly organizationId: OrganizationId;
964
+ /**
965
+ * The active organization new conversations land in, or `null` when none is
966
+ * selected — reads work either way.
967
+ */
968
+ readonly organizationId: OrganizationId | null;
954
969
  /**
955
970
  * Who the session says is acting — awaited, because the first `getSession()`
956
971
  * is a real read. Rejects with `session-unavailable` when nobody is signed
@@ -1107,11 +1122,18 @@ interface MessagingSnapshot {
1107
1122
  } | null;
1108
1123
  /** Conversations whose last open failed, keyed by id. Cleared on the next open. */
1109
1124
  readonly conversationReadFailures: ReadonlyMap<ConversationId, ConversationReadFailure>;
1125
+ /**
1126
+ * The inbox read FAILED (and no later read succeeded). Without it a failed
1127
+ * first read left `hasLoadedConversations` false forever — a skeleton that
1128
+ * never resolves is a screen telling a lie (RC-B12 round 11).
1129
+ */
1130
+ readonly conversationsLoadError: string | null;
1110
1131
  }
1111
1132
  interface MessagingStore {
1112
1133
  snapshot(): MessagingSnapshot;
1113
1134
  subscribe(listener: (snapshot: MessagingSnapshot) => void): () => void;
1114
1135
  setConversations(items: readonly ConversationSummary[], hasMore: boolean): void;
1136
+ setConversationsLoadError(message: string | null): void;
1115
1137
  /**
1116
1138
  * Switch the archive axis. Clears the list and marks it UNLOADED: the rows
1117
1139
  * on screen belong to the old filter, and leaving them there while the new
@@ -1221,6 +1243,8 @@ interface MessagingEngine {
1221
1243
  * refreshes the archived count the reveal label prints.
1222
1244
  */
1223
1245
  setArchiveFilter(next: MessagingArchiveFilter): Promise<void>;
1246
+ /** Read the inbox again (the failed-read view's Try again). Never throws; a failure lands in the snapshot. */
1247
+ refreshInbox(): Promise<void>;
1224
1248
  /** Re-read how many archived conversations exist, for the reveal's label. */
1225
1249
  refreshArchivedCount(): Promise<void>;
1226
1250
  /** Open a conversation: load its thread and subscribe to its channel. */
@@ -1311,9 +1335,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
1311
1335
 
1312
1336
  declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
1313
1337
  declare function projectMessageAction(value: unknown): MessageAction | null;
1314
- declare function projectMessage(row: Record<string, unknown>, fallbackOrganizationId: OrganizationId): Message;
1338
+ declare function projectMessage(row: Record<string, unknown>,
1339
+ /** The active organization, or `null` when none is selected (reads never need one). */
1340
+ fallbackOrganizationId: OrganizationId | null): Message;
1315
1341
  declare function projectParticipantRole(value: unknown): ParticipantRole;
1316
- declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId, fallbackOrganizationId: OrganizationId): ConversationSummary;
1342
+ declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
1343
+ /** The active organization, or `null` when none is selected (reads never need one). */
1344
+ fallbackOrganizationId: OrganizationId | null): ConversationSummary;
1317
1345
 
1318
1346
  /**
1319
1347
  * MATRX REFERENCES — a message can name a platform entity, and everything it
package/dist/index.d.ts CHANGED
@@ -207,7 +207,11 @@ interface Page<TItem, TCursor> {
207
207
  /** What the host injects. Identity ONLY — every hard part is in the package. */
208
208
  interface MessagingIdentity {
209
209
  readonly userId: UserId;
210
- readonly organizationId: OrganizationId;
210
+ /**
211
+ * The active organization, or `null` when none is selected. Reads never
212
+ * depend on it; it is only where a NEW conversation lands.
213
+ */
214
+ readonly organizationId: OrganizationId | null;
211
215
  }
212
216
  interface DraftMessage {
213
217
  readonly conversationId: ConversationId;
@@ -628,7 +632,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
628
632
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
629
633
  * warns and retries; it is never a red error.
630
634
  */
631
- type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
635
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
636
+ /** A NEW conversation needs an organization to land in and none is selected. */
637
+ | "organization-required" | "unknown";
632
638
  declare class MessagingError extends Error {
633
639
  readonly code: MessagingErrorCode;
634
640
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
@@ -920,15 +926,21 @@ interface RepositoryOptions {
920
926
  */
921
927
  client: MessagingSupabaseClient;
922
928
  /**
923
- * The active organization. Every conversation and message write carries it
924
- * explicitly (R5).
929
+ * The active organization, or `null`/omitted when none is selected.
930
+ *
931
+ * 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
932
+ * read here is answered by RLS as the session's user; the organization is
933
+ * only the DESTINATION of a NEW conversation. Without one, starting a
934
+ * conversation is refused with `organization-required` (the host asks the
935
+ * person to choose), and a message or member added to an EXISTING
936
+ * conversation is stamped with that conversation's own organization.
925
937
  *
926
938
  * 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
927
939
  * acting is read from `client`'s own session — see `session-identity.ts` for
928
940
  * the class that closed and why a handed-in id could not be made safe by
929
941
  * convention. A caller that still passes `identity` is IGNORED and told so.
930
942
  */
931
- organizationId: string;
943
+ organizationId?: string | null | undefined;
932
944
  /**
933
945
  * INTERNAL. The React provider already runs one session reader for its own
934
946
  * render loop and hands it here so the two cannot answer differently and so
@@ -949,8 +961,11 @@ interface RepositoryOptions {
949
961
  now?: (() => number) | undefined;
950
962
  }
951
963
  interface MessagingRepository {
952
- /** The organization every write here is stamped with. */
953
- readonly organizationId: OrganizationId;
964
+ /**
965
+ * The active organization new conversations land in, or `null` when none is
966
+ * selected — reads work either way.
967
+ */
968
+ readonly organizationId: OrganizationId | null;
954
969
  /**
955
970
  * Who the session says is acting — awaited, because the first `getSession()`
956
971
  * is a real read. Rejects with `session-unavailable` when nobody is signed
@@ -1107,11 +1122,18 @@ interface MessagingSnapshot {
1107
1122
  } | null;
1108
1123
  /** Conversations whose last open failed, keyed by id. Cleared on the next open. */
1109
1124
  readonly conversationReadFailures: ReadonlyMap<ConversationId, ConversationReadFailure>;
1125
+ /**
1126
+ * The inbox read FAILED (and no later read succeeded). Without it a failed
1127
+ * first read left `hasLoadedConversations` false forever — a skeleton that
1128
+ * never resolves is a screen telling a lie (RC-B12 round 11).
1129
+ */
1130
+ readonly conversationsLoadError: string | null;
1110
1131
  }
1111
1132
  interface MessagingStore {
1112
1133
  snapshot(): MessagingSnapshot;
1113
1134
  subscribe(listener: (snapshot: MessagingSnapshot) => void): () => void;
1114
1135
  setConversations(items: readonly ConversationSummary[], hasMore: boolean): void;
1136
+ setConversationsLoadError(message: string | null): void;
1115
1137
  /**
1116
1138
  * Switch the archive axis. Clears the list and marks it UNLOADED: the rows
1117
1139
  * on screen belong to the old filter, and leaving them there while the new
@@ -1221,6 +1243,8 @@ interface MessagingEngine {
1221
1243
  * refreshes the archived count the reveal label prints.
1222
1244
  */
1223
1245
  setArchiveFilter(next: MessagingArchiveFilter): Promise<void>;
1246
+ /** Read the inbox again (the failed-read view's Try again). Never throws; a failure lands in the snapshot. */
1247
+ refreshInbox(): Promise<void>;
1224
1248
  /** Re-read how many archived conversations exist, for the reveal's label. */
1225
1249
  refreshArchivedCount(): Promise<void>;
1226
1250
  /** Open a conversation: load its thread and subscribe to its channel. */
@@ -1311,9 +1335,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
1311
1335
 
1312
1336
  declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
1313
1337
  declare function projectMessageAction(value: unknown): MessageAction | null;
1314
- declare function projectMessage(row: Record<string, unknown>, fallbackOrganizationId: OrganizationId): Message;
1338
+ declare function projectMessage(row: Record<string, unknown>,
1339
+ /** The active organization, or `null` when none is selected (reads never need one). */
1340
+ fallbackOrganizationId: OrganizationId | null): Message;
1315
1341
  declare function projectParticipantRole(value: unknown): ParticipantRole;
1316
- declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId, fallbackOrganizationId: OrganizationId): ConversationSummary;
1342
+ declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
1343
+ /** The active organization, or `null` when none is selected (reads never need one). */
1344
+ fallbackOrganizationId: OrganizationId | null): ConversationSummary;
1317
1345
 
1318
1346
  /**
1319
1347
  * MATRX REFERENCES — a message can name a platform entity, and everything it
package/dist/index.js CHANGED
@@ -861,7 +861,7 @@ function projectMessage(row, fallbackOrganizationId) {
861
861
  id: requiredStr(row, "id", operation),
862
862
  conversationId: requiredStr(row, "conversation_id", operation),
863
863
  senderId: requiredStr(row, "sender_id", operation),
864
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
864
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
865
865
  content: typeof row["content"] === "string" ? row["content"] : "",
866
866
  kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
867
867
  // A row read from the DB is at least `sent`. `sending`/`failed` are outbox
@@ -909,7 +909,7 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
909
909
  groupName,
910
910
  groupImageUrl,
911
911
  createdBy: createdBy === null ? null : createdBy,
912
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
912
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
913
913
  createdAt,
914
914
  updatedAt,
915
915
  metadata: jsonObject(row["metadata"])
@@ -975,6 +975,7 @@ function createMessagingStore() {
975
975
  let threads = /* @__PURE__ */ new Map();
976
976
  let activeConversationId = null;
977
977
  let conversationReadFailures = /* @__PURE__ */ new Map();
978
+ let conversationsLoadError = null;
978
979
  const listeners = /* @__PURE__ */ new Set();
979
980
  let cached = null;
980
981
  function snapshot() {
@@ -988,7 +989,8 @@ function createMessagingStore() {
988
989
  totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length,
989
990
  archiveFilter,
990
991
  archivedCount,
991
- conversationReadFailures
992
+ conversationReadFailures,
993
+ conversationsLoadError
992
994
  };
993
995
  return cached;
994
996
  }
@@ -1033,6 +1035,12 @@ function createMessagingStore() {
1033
1035
  conversations = normalizeConversations(items);
1034
1036
  hasMoreConversations = hasMore;
1035
1037
  hasLoadedConversations = true;
1038
+ conversationsLoadError = null;
1039
+ emit();
1040
+ },
1041
+ setConversationsLoadError(message) {
1042
+ if (message === conversationsLoadError) return;
1043
+ conversationsLoadError = message;
1036
1044
  emit();
1037
1045
  },
1038
1046
  setArchiveFilter(next) {
@@ -1242,7 +1250,11 @@ function createMessagingEngine(options) {
1242
1250
  optimisticMessage({
1243
1251
  conversationId: entry.draft.conversationId,
1244
1252
  senderId: identity.userId,
1245
- organizationId: identity.organizationId,
1253
+ // Client-only until the confirmed row replaces it: the active
1254
+ // organization, else the conversation's own.
1255
+ organizationId: identity.organizationId ?? store.snapshot().conversations.find(
1256
+ (item) => item.conversation.id === entry.draft.conversationId
1257
+ )?.conversation.organizationId ?? "",
1246
1258
  content: entry.draft.content,
1247
1259
  clientMessageId: entry.clientMessageId,
1248
1260
  ...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
@@ -1261,10 +1273,20 @@ function createMessagingEngine(options) {
1261
1273
  }
1262
1274
  async function reloadInbox() {
1263
1275
  if (disposed) return;
1264
- const page = await repository.listConversations({
1265
- limit: conversationPageSize,
1266
- archived: store.snapshot().archiveFilter
1267
- });
1276
+ let page;
1277
+ try {
1278
+ page = await repository.listConversations({
1279
+ limit: conversationPageSize,
1280
+ archived: store.snapshot().archiveFilter
1281
+ });
1282
+ } catch (error) {
1283
+ if (!disposed) {
1284
+ store.setConversationsLoadError(
1285
+ error instanceof Error ? error.message : typeof error === "string" ? error : "The conversation list could not be read."
1286
+ );
1287
+ }
1288
+ throw error;
1289
+ }
1268
1290
  if (disposed) return;
1269
1291
  conversationCursor = page.nextCursor;
1270
1292
  store.setConversations(page.items, page.hasMore);
@@ -1349,6 +1371,13 @@ function createMessagingEngine(options) {
1349
1371
  }
1350
1372
  });
1351
1373
  },
1374
+ async refreshInbox() {
1375
+ try {
1376
+ await reloadInbox();
1377
+ } catch (error) {
1378
+ reportError(error, "inboxRefresh");
1379
+ }
1380
+ },
1352
1381
  async setArchiveFilter(next) {
1353
1382
  if (next === store.snapshot().archiveFilter) return;
1354
1383
  store.setArchiveFilter(next);
@@ -1774,12 +1803,15 @@ var RPCS = {
1774
1803
  };
1775
1804
  var MESSAGE_COLUMNS = "id,conversation_id,sender_id,organization_id,content,message_type,status,reply_to_id,client_message_id,action_data,media_url,media_thumbnail_url,media_metadata,created_at,edited_at,deleted_at,deleted_for_everyone,metadata";
1776
1805
  var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
1806
+ function optionalOrg(organizationId) {
1807
+ return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
1808
+ }
1777
1809
  function requireOrg(organizationId, operation) {
1778
- if (typeof organizationId !== "string" || organizationId.length === 0) {
1810
+ if (organizationId === null) {
1779
1811
  throw new MessagingError(
1780
- "misconfigured",
1781
- `${operation}: no organization_id`,
1782
- "Every conversation and message write carries an explicit organization_id (R5). Pass a real org on <MessagingProvider>; the package refuses the write rather than letting an unscoped row reach the database."
1812
+ "organization-required",
1813
+ `${operation}: no organization selected`,
1814
+ "A new conversation is filed under one organization. Ask the person to choose one, then pass it as organizationId on <MessagingProvider>. Reading and replying to existing conversations never needs it."
1783
1815
  );
1784
1816
  }
1785
1817
  return organizationId;
@@ -1791,7 +1823,7 @@ function sleep(ms) {
1791
1823
  return new Promise((resolve) => setTimeout(resolve, jittered));
1792
1824
  }
1793
1825
  function createMessagingRepository(options) {
1794
- const org = requireOrg(options.organizationId, "createMessagingRepository");
1826
+ const org = optionalOrg(options.organizationId);
1795
1827
  const handedIn = options.identity;
1796
1828
  if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
1797
1829
  options.onDiagnostic?.(
@@ -1861,6 +1893,11 @@ function createMessagingRepository(options) {
1861
1893
  });
1862
1894
  return found;
1863
1895
  }
1896
+ async function organizationOfExisting(conversationId) {
1897
+ if (org !== null) return org;
1898
+ const conversation = await repository.getConversation(conversationId);
1899
+ return conversation.organizationId;
1900
+ }
1864
1901
  const repository = {
1865
1902
  organizationId: org,
1866
1903
  actingUserId,
@@ -1993,6 +2030,7 @@ function createMessagingRepository(options) {
1993
2030
  },
1994
2031
  async getOrCreateDirectConversation(otherUserId) {
1995
2032
  const operation = "getOrCreateDirectConversation";
2033
+ const destination = requireOrg(org, operation);
1996
2034
  const me = await actingUser(operation);
1997
2035
  const id = await withSessionRetry(
1998
2036
  operation,
@@ -2004,7 +2042,7 @@ function createMessagingRepository(options) {
2004
2042
  // not a preference.
2005
2043
  p_user1_id: me,
2006
2044
  p_user2_id: otherUserId,
2007
- p_organization_id: org
2045
+ p_organization_id: destination
2008
2046
  },
2009
2047
  operation
2010
2048
  )
@@ -2016,13 +2054,14 @@ function createMessagingRepository(options) {
2016
2054
  },
2017
2055
  async createGroupConversation({ name, memberIds }) {
2018
2056
  const operation = "createGroupConversation";
2057
+ const destination = requireOrg(org, operation);
2019
2058
  const me = await actingUser(operation);
2020
2059
  const { data, error } = await withSessionRetry(
2021
2060
  operation,
2022
2061
  async () => db().from(TABLES.conversations).insert({
2023
2062
  type: "group",
2024
2063
  group_name: name,
2025
- organization_id: org,
2064
+ organization_id: destination,
2026
2065
  created_by: me
2027
2066
  }).select("id").single()
2028
2067
  );
@@ -2037,7 +2076,7 @@ function createMessagingRepository(options) {
2037
2076
  conversation_id: conversationId,
2038
2077
  user_id: userId,
2039
2078
  role: userId === me ? "owner" : "member",
2040
- organization_id: org,
2079
+ organization_id: destination,
2041
2080
  created_by: me
2042
2081
  }))
2043
2082
  );
@@ -2047,10 +2086,11 @@ function createMessagingRepository(options) {
2047
2086
  async insertMessage(draft, clientMessageId) {
2048
2087
  const operation = "insertMessage";
2049
2088
  const me = await actingUser(operation);
2089
+ const destination = await organizationOfExisting(draft.conversationId);
2050
2090
  const { data, error } = await db().from(TABLES.messages).insert({
2051
2091
  conversation_id: draft.conversationId,
2052
2092
  sender_id: me,
2053
- organization_id: org,
2093
+ organization_id: destination,
2054
2094
  created_by: me,
2055
2095
  content: draft.content,
2056
2096
  message_type: draft.kind ?? "text",
@@ -2111,12 +2151,13 @@ function createMessagingRepository(options) {
2111
2151
  async addMembers(conversationId, memberIds) {
2112
2152
  const operation = "addMembers";
2113
2153
  const me = await actingUser(operation);
2154
+ const destination = await organizationOfExisting(conversationId);
2114
2155
  const { error } = await db().from(TABLES.participants).insert(
2115
2156
  memberIds.map((userId) => ({
2116
2157
  conversation_id: conversationId,
2117
2158
  user_id: userId,
2118
2159
  role: "member",
2119
- organization_id: org,
2160
+ organization_id: destination,
2120
2161
  created_by: me
2121
2162
  }))
2122
2163
  );