@ai-matrx/messaging 0.13.0 → 0.14.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/CHANGELOG.md +47 -0
- package/README.md +5 -1
- package/dist/index.cjs +30 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -9
- package/dist/index.d.ts +28 -9
- package/dist/index.js +30 -14
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +43 -24
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +34 -10
- package/dist/react.d.ts +34 -10
- package/dist/react.js +43 -24
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog — `@ai-matrx/messaging`
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Messaging runs with no organization selected** (access belongs to the
|
|
8
|
+
person, 2026-09-23). `<MessagingProvider organizationId={null}>` used to stay
|
|
9
|
+
inert — every inbox and conversation spun forever — and
|
|
10
|
+
`createMessagingRepository` threw at construction without an org, although
|
|
11
|
+
no read here filters by organization. Now the provider starts and every read
|
|
12
|
+
works as the signed-in person. The organization is only a write destination:
|
|
13
|
+
- starting a NEW conversation (`getOrCreateDirectConversation`,
|
|
14
|
+
`createGroupConversation`, `useConversations().startDirect`) without one is
|
|
15
|
+
refused with the new error code **`organization-required`**, before
|
|
16
|
+
anything reaches the database;
|
|
17
|
+
- a message or member added to an EXISTING conversation with no active
|
|
18
|
+
organization is stamped with that conversation's own organization;
|
|
19
|
+
- AI actions do not render without an organization (a run is filed under one).
|
|
20
|
+
- `MessagingIdentity.organizationId` and `MessagingRepository.organizationId`
|
|
21
|
+
are now `OrganizationId | null`; `RepositoryOptions.organizationId` is
|
|
22
|
+
optional. `MessagingErrorCode` gains `"organization-required"`.
|
|
23
|
+
|
|
24
|
+
### Consumer action
|
|
25
|
+
|
|
26
|
+
- Pass the active organization as before, including `null` when none is
|
|
27
|
+
selected — do not gate `<MessagingProvider>` on having one.
|
|
28
|
+
- Where the host starts a conversation, ask the person to choose an
|
|
29
|
+
organization when none is selected (or handle `error.code ===
|
|
30
|
+
"organization-required"`).
|
|
31
|
+
- Code that read `host.identity.organizationId` as non-null must handle `null`.
|
|
32
|
+
|
|
33
|
+
## 0.13.1
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Opening an unavailable conversation no longer loops.** 0.13.0 cleared a
|
|
38
|
+
conversation's read failure at the START of every `openConversation`, so a
|
|
39
|
+
second holder (the page and its thread pane both calling `useConversation`)
|
|
40
|
+
flipped `unavailable` back to false while its read was in flight. A host that
|
|
41
|
+
swaps the thread for an access surface on that flag remounted the thread,
|
|
42
|
+
which re-opened, which cleared it again — "Maximum update depth exceeded" on
|
|
43
|
+
matrx-frontend `/messages/<id>`. The failure is now replaced only by the next
|
|
44
|
+
answer.
|
|
45
|
+
|
|
46
|
+
### Consumer action
|
|
47
|
+
|
|
48
|
+
- None — adopt the release.
|
|
49
|
+
|
|
3
50
|
## 0.13.0
|
|
4
51
|
|
|
5
52
|
### Added
|
package/README.md
CHANGED
|
@@ -82,7 +82,11 @@ notifications race it into two "direct" conversations for one pair, forever.
|
|
|
82
82
|
RLS plus auth-checked SECURITY DEFINER RPCs. There is not one permission branch in this package —
|
|
83
83
|
an RPC that says no becomes a `forbidden` error, never a client-side re-decision. And every
|
|
84
84
|
conversation and message write carries an explicit `organization_id`; the package refuses the
|
|
85
|
-
write rather than letting an unscoped row reach the database.
|
|
85
|
+
write rather than letting an unscoped row reach the database. The organization is a write
|
|
86
|
+
DESTINATION only: `organizationId={null}` is a valid state in which every read works (the inbox,
|
|
87
|
+
a deep-linked conversation, replying — a reply is stamped with its conversation's organization),
|
|
88
|
+
and starting a NEW conversation is refused as `organization-required` so the host can ask the
|
|
89
|
+
person to choose one.
|
|
86
90
|
|
|
87
91
|
### 7. A missing session is a lifecycle moment, not an error.
|
|
88
92
|
|
package/dist/index.cjs
CHANGED
|
@@ -931,7 +931,7 @@ function projectMessage(row, fallbackOrganizationId) {
|
|
|
931
931
|
id: requiredStr(row, "id", operation),
|
|
932
932
|
conversationId: requiredStr(row, "conversation_id", operation),
|
|
933
933
|
senderId: requiredStr(row, "sender_id", operation),
|
|
934
|
-
organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
|
|
934
|
+
organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
|
|
935
935
|
content: typeof row["content"] === "string" ? row["content"] : "",
|
|
936
936
|
kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
|
|
937
937
|
// A row read from the DB is at least `sent`. `sending`/`failed` are outbox
|
|
@@ -979,7 +979,7 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
|
|
|
979
979
|
groupName,
|
|
980
980
|
groupImageUrl,
|
|
981
981
|
createdBy: createdBy === null ? null : createdBy,
|
|
982
|
-
organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
|
|
982
|
+
organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
|
|
983
983
|
createdAt,
|
|
984
984
|
updatedAt,
|
|
985
985
|
metadata: jsonObject(row["metadata"])
|
|
@@ -1312,7 +1312,11 @@ function createMessagingEngine(options) {
|
|
|
1312
1312
|
optimisticMessage({
|
|
1313
1313
|
conversationId: entry.draft.conversationId,
|
|
1314
1314
|
senderId: identity.userId,
|
|
1315
|
-
|
|
1315
|
+
// Client-only until the confirmed row replaces it: the active
|
|
1316
|
+
// organization, else the conversation's own.
|
|
1317
|
+
organizationId: identity.organizationId ?? store.snapshot().conversations.find(
|
|
1318
|
+
(item) => item.conversation.id === entry.draft.conversationId
|
|
1319
|
+
)?.conversation.organizationId ?? "",
|
|
1316
1320
|
content: entry.draft.content,
|
|
1317
1321
|
clientMessageId: entry.clientMessageId,
|
|
1318
1322
|
...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
|
|
@@ -1455,12 +1459,12 @@ function createMessagingEngine(options) {
|
|
|
1455
1459
|
},
|
|
1456
1460
|
async openConversation(id) {
|
|
1457
1461
|
store.setActiveConversation(id);
|
|
1458
|
-
store.setConversationReadFailure(id, null);
|
|
1459
1462
|
try {
|
|
1460
1463
|
const page = await repository.listMessages(id, { limit: messagePageSize });
|
|
1461
1464
|
if (page.items.length === 0) {
|
|
1462
1465
|
await repository.getConversation(id);
|
|
1463
1466
|
}
|
|
1467
|
+
store.setConversationReadFailure(id, null);
|
|
1464
1468
|
store.setThread(id, page.items, { hasMoreOlder: page.hasMore });
|
|
1465
1469
|
} catch (error) {
|
|
1466
1470
|
const normalized = normalizeMessagingError(error, "openConversation");
|
|
@@ -1844,12 +1848,15 @@ var RPCS = {
|
|
|
1844
1848
|
};
|
|
1845
1849
|
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";
|
|
1846
1850
|
var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
|
|
1851
|
+
function optionalOrg(organizationId) {
|
|
1852
|
+
return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
|
|
1853
|
+
}
|
|
1847
1854
|
function requireOrg(organizationId, operation) {
|
|
1848
|
-
if (
|
|
1855
|
+
if (organizationId === null) {
|
|
1849
1856
|
throw new MessagingError(
|
|
1850
|
-
"
|
|
1851
|
-
`${operation}: no
|
|
1852
|
-
"
|
|
1857
|
+
"organization-required",
|
|
1858
|
+
`${operation}: no organization selected`,
|
|
1859
|
+
"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."
|
|
1853
1860
|
);
|
|
1854
1861
|
}
|
|
1855
1862
|
return organizationId;
|
|
@@ -1861,7 +1868,7 @@ function sleep(ms) {
|
|
|
1861
1868
|
return new Promise((resolve) => setTimeout(resolve, jittered));
|
|
1862
1869
|
}
|
|
1863
1870
|
function createMessagingRepository(options) {
|
|
1864
|
-
const org =
|
|
1871
|
+
const org = optionalOrg(options.organizationId);
|
|
1865
1872
|
const handedIn = options.identity;
|
|
1866
1873
|
if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
|
|
1867
1874
|
options.onDiagnostic?.(
|
|
@@ -1931,6 +1938,11 @@ function createMessagingRepository(options) {
|
|
|
1931
1938
|
});
|
|
1932
1939
|
return found;
|
|
1933
1940
|
}
|
|
1941
|
+
async function organizationOfExisting(conversationId) {
|
|
1942
|
+
if (org !== null) return org;
|
|
1943
|
+
const conversation = await repository.getConversation(conversationId);
|
|
1944
|
+
return conversation.organizationId;
|
|
1945
|
+
}
|
|
1934
1946
|
const repository = {
|
|
1935
1947
|
organizationId: org,
|
|
1936
1948
|
actingUserId,
|
|
@@ -2063,6 +2075,7 @@ function createMessagingRepository(options) {
|
|
|
2063
2075
|
},
|
|
2064
2076
|
async getOrCreateDirectConversation(otherUserId) {
|
|
2065
2077
|
const operation = "getOrCreateDirectConversation";
|
|
2078
|
+
const destination = requireOrg(org, operation);
|
|
2066
2079
|
const me = await actingUser(operation);
|
|
2067
2080
|
const id = await withSessionRetry(
|
|
2068
2081
|
operation,
|
|
@@ -2074,7 +2087,7 @@ function createMessagingRepository(options) {
|
|
|
2074
2087
|
// not a preference.
|
|
2075
2088
|
p_user1_id: me,
|
|
2076
2089
|
p_user2_id: otherUserId,
|
|
2077
|
-
p_organization_id:
|
|
2090
|
+
p_organization_id: destination
|
|
2078
2091
|
},
|
|
2079
2092
|
operation
|
|
2080
2093
|
)
|
|
@@ -2086,13 +2099,14 @@ function createMessagingRepository(options) {
|
|
|
2086
2099
|
},
|
|
2087
2100
|
async createGroupConversation({ name, memberIds }) {
|
|
2088
2101
|
const operation = "createGroupConversation";
|
|
2102
|
+
const destination = requireOrg(org, operation);
|
|
2089
2103
|
const me = await actingUser(operation);
|
|
2090
2104
|
const { data, error } = await withSessionRetry(
|
|
2091
2105
|
operation,
|
|
2092
2106
|
async () => db().from(TABLES.conversations).insert({
|
|
2093
2107
|
type: "group",
|
|
2094
2108
|
group_name: name,
|
|
2095
|
-
organization_id:
|
|
2109
|
+
organization_id: destination,
|
|
2096
2110
|
created_by: me
|
|
2097
2111
|
}).select("id").single()
|
|
2098
2112
|
);
|
|
@@ -2107,7 +2121,7 @@ function createMessagingRepository(options) {
|
|
|
2107
2121
|
conversation_id: conversationId,
|
|
2108
2122
|
user_id: userId,
|
|
2109
2123
|
role: userId === me ? "owner" : "member",
|
|
2110
|
-
organization_id:
|
|
2124
|
+
organization_id: destination,
|
|
2111
2125
|
created_by: me
|
|
2112
2126
|
}))
|
|
2113
2127
|
);
|
|
@@ -2117,10 +2131,11 @@ function createMessagingRepository(options) {
|
|
|
2117
2131
|
async insertMessage(draft, clientMessageId) {
|
|
2118
2132
|
const operation = "insertMessage";
|
|
2119
2133
|
const me = await actingUser(operation);
|
|
2134
|
+
const destination = await organizationOfExisting(draft.conversationId);
|
|
2120
2135
|
const { data, error } = await db().from(TABLES.messages).insert({
|
|
2121
2136
|
conversation_id: draft.conversationId,
|
|
2122
2137
|
sender_id: me,
|
|
2123
|
-
organization_id:
|
|
2138
|
+
organization_id: destination,
|
|
2124
2139
|
created_by: me,
|
|
2125
2140
|
content: draft.content,
|
|
2126
2141
|
message_type: draft.kind ?? "text",
|
|
@@ -2181,12 +2196,13 @@ function createMessagingRepository(options) {
|
|
|
2181
2196
|
async addMembers(conversationId, memberIds) {
|
|
2182
2197
|
const operation = "addMembers";
|
|
2183
2198
|
const me = await actingUser(operation);
|
|
2199
|
+
const destination = await organizationOfExisting(conversationId);
|
|
2184
2200
|
const { error } = await db().from(TABLES.participants).insert(
|
|
2185
2201
|
memberIds.map((userId) => ({
|
|
2186
2202
|
conversation_id: conversationId,
|
|
2187
2203
|
user_id: userId,
|
|
2188
2204
|
role: "member",
|
|
2189
|
-
organization_id:
|
|
2205
|
+
organization_id: destination,
|
|
2190
2206
|
created_by: me
|
|
2191
2207
|
}))
|
|
2192
2208
|
);
|