@headless-lms/core 0.4.0 → 0.5.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.
Files changed (249) hide show
  1. package/dist/assets/events.d.ts +27 -21
  2. package/dist/assets/file-urls.d.ts +11 -7
  3. package/dist/assets/file-urls.js +14 -4
  4. package/dist/assets/file-urls.js.map +1 -1
  5. package/dist/assets/index.d.ts +4 -4
  6. package/dist/assets/index.js +3 -3
  7. package/dist/assets/ports.d.ts +34 -15
  8. package/dist/assets/service.d.ts +30 -14
  9. package/dist/assets/service.js +230 -56
  10. package/dist/assets/service.js.map +1 -1
  11. package/dist/automations/actions.js.map +1 -1
  12. package/dist/automations/catalog.js +69 -4
  13. package/dist/automations/catalog.js.map +1 -1
  14. package/dist/automations/community-derivations.js +16 -16
  15. package/dist/automations/community-derivations.js.map +1 -1
  16. package/dist/automations/community-mail.d.ts +34 -4
  17. package/dist/automations/community-mail.js +32 -4
  18. package/dist/automations/community-mail.js.map +1 -1
  19. package/dist/automations/event-permissions.d.ts +12 -0
  20. package/dist/automations/event-permissions.js +38 -0
  21. package/dist/automations/event-permissions.js.map +1 -0
  22. package/dist/automations/index.d.ts +2 -1
  23. package/dist/automations/index.js +2 -1
  24. package/dist/automations/notifications/applicable.d.ts +8 -0
  25. package/dist/automations/notifications/applicable.js +12 -0
  26. package/dist/automations/notifications/applicable.js.map +1 -0
  27. package/dist/automations/notifications/debounce.js +1 -1
  28. package/dist/automations/notifications/debounce.js.map +1 -1
  29. package/dist/automations/notifications/defaults.js +33 -3
  30. package/dist/automations/notifications/defaults.js.map +1 -1
  31. package/dist/automations/notifications/processor.js +3 -3
  32. package/dist/automations/notifications/processor.js.map +1 -1
  33. package/dist/automations/notifications/rules/content.js +60 -3
  34. package/dist/automations/notifications/rules/content.js.map +1 -1
  35. package/dist/automations/notifications/rules/discussion.js +29 -29
  36. package/dist/automations/notifications/rules/discussion.js.map +1 -1
  37. package/dist/automations/notifications/rules/entitlements.js +36 -9
  38. package/dist/automations/notifications/rules/entitlements.js.map +1 -1
  39. package/dist/automations/notifications/rules/events.d.ts +9 -0
  40. package/dist/automations/notifications/rules/events.js +27 -0
  41. package/dist/automations/notifications/rules/events.js.map +1 -0
  42. package/dist/automations/notifications/rules/organizations.js +17 -10
  43. package/dist/automations/notifications/rules/organizations.js.map +1 -1
  44. package/dist/automations/notifications/rules/recipients.js +0 -1
  45. package/dist/automations/notifications/rules/recipients.js.map +1 -1
  46. package/dist/automations/notifications/service.d.ts +8 -6
  47. package/dist/automations/notifications/service.js +13 -13
  48. package/dist/automations/notifications/service.js.map +1 -1
  49. package/dist/automations/notifications/types.js +94 -7
  50. package/dist/automations/notifications/types.js.map +1 -1
  51. package/dist/automations/ports.d.ts +30 -31
  52. package/dist/automations/service.d.ts +20 -17
  53. package/dist/automations/service.js +91 -62
  54. package/dist/automations/service.js.map +1 -1
  55. package/dist/content/event-sight.d.ts +11 -0
  56. package/dist/content/event-sight.js +11 -0
  57. package/dist/content/event-sight.js.map +1 -0
  58. package/dist/content/events.d.ts +413 -72
  59. package/dist/content/events.js +34 -1
  60. package/dist/content/events.js.map +1 -1
  61. package/dist/content/index.d.ts +6 -4
  62. package/dist/content/index.js +3 -1
  63. package/dist/content/jobs.d.ts +16 -0
  64. package/dist/content/jobs.js +37 -0
  65. package/dist/content/jobs.js.map +1 -0
  66. package/dist/content/model.d.ts +2 -2
  67. package/dist/content/notification-categories.d.ts +5 -4
  68. package/dist/content/notification-categories.js +15 -6
  69. package/dist/content/notification-categories.js.map +1 -1
  70. package/dist/content/ports.d.ts +113 -68
  71. package/dist/content/service.d.ts +99 -67
  72. package/dist/content/service.js +680 -139
  73. package/dist/content/service.js.map +1 -1
  74. package/dist/content/titles.d.ts +6 -3
  75. package/dist/content/titles.js +37 -24
  76. package/dist/content/titles.js.map +1 -1
  77. package/dist/content/types.d.ts +2 -2
  78. package/dist/discussion/authors.d.ts +4 -2
  79. package/dist/discussion/authors.js +9 -5
  80. package/dist/discussion/authors.js.map +1 -1
  81. package/dist/discussion/chats.d.ts +56 -37
  82. package/dist/discussion/chats.js +393 -161
  83. package/dist/discussion/chats.js.map +1 -1
  84. package/dist/discussion/events.d.ts +77 -39
  85. package/dist/discussion/events.js +41 -3
  86. package/dist/discussion/events.js.map +1 -1
  87. package/dist/discussion/index.d.ts +8 -5
  88. package/dist/discussion/index.js +5 -3
  89. package/dist/discussion/live.d.ts +106 -0
  90. package/dist/discussion/live.js +642 -0
  91. package/dist/discussion/live.js.map +1 -0
  92. package/dist/discussion/mentions.js +2 -2
  93. package/dist/discussion/mentions.js.map +1 -1
  94. package/dist/discussion/ports.d.ts +182 -100
  95. package/dist/discussion/ports.js.map +1 -1
  96. package/dist/discussion/posts.d.ts +45 -39
  97. package/dist/discussion/posts.js +268 -154
  98. package/dist/discussion/posts.js.map +1 -1
  99. package/dist/discussion/rules.d.ts +18 -0
  100. package/dist/discussion/rules.js +22 -0
  101. package/dist/discussion/rules.js.map +1 -0
  102. package/dist/discussion/service.d.ts +37 -31
  103. package/dist/discussion/service.js +252 -188
  104. package/dist/discussion/service.js.map +1 -1
  105. package/dist/discussion/subscriptions.d.ts +8 -8
  106. package/dist/discussion/subscriptions.js +29 -13
  107. package/dist/discussion/subscriptions.js.map +1 -1
  108. package/dist/discussion/titles.d.ts +4 -2
  109. package/dist/discussion/titles.js +27 -13
  110. package/dist/discussion/titles.js.map +1 -1
  111. package/dist/entitlements/events.d.ts +20 -20
  112. package/dist/entitlements/index.d.ts +4 -4
  113. package/dist/entitlements/index.js +2 -2
  114. package/dist/entitlements/model.d.ts +2 -2
  115. package/dist/entitlements/ports.d.ts +65 -39
  116. package/dist/entitlements/rules.d.ts +3 -6
  117. package/dist/entitlements/rules.js +5 -10
  118. package/dist/entitlements/rules.js.map +1 -1
  119. package/dist/entitlements/service.d.ts +40 -28
  120. package/dist/entitlements/service.js +370 -163
  121. package/dist/entitlements/service.js.map +1 -1
  122. package/dist/identity/devices.d.ts +3 -3
  123. package/dist/identity/devices.js +7 -5
  124. package/dist/identity/devices.js.map +1 -1
  125. package/dist/identity/events.d.ts +2 -2
  126. package/dist/identity/index.d.ts +2 -2
  127. package/dist/identity/index.js +2 -2
  128. package/dist/identity/notification-settings.d.ts +1 -1
  129. package/dist/identity/notification-settings.js +3 -2
  130. package/dist/identity/notification-settings.js.map +1 -1
  131. package/dist/identity/ports.d.ts +7 -7
  132. package/dist/identity/service.d.ts +8 -7
  133. package/dist/identity/service.js +17 -13
  134. package/dist/identity/service.js.map +1 -1
  135. package/dist/integrations/events.d.ts +3 -3
  136. package/dist/integrations/ports.d.ts +9 -8
  137. package/dist/integrations/service.d.ts +9 -8
  138. package/dist/integrations/service.js +33 -16
  139. package/dist/integrations/service.js.map +1 -1
  140. package/dist/organizations/default-roles.d.ts +22 -0
  141. package/dist/organizations/default-roles.js +89 -0
  142. package/dist/organizations/default-roles.js.map +1 -0
  143. package/dist/organizations/effective-permissions.d.ts +45 -0
  144. package/dist/organizations/effective-permissions.js +113 -0
  145. package/dist/organizations/effective-permissions.js.map +1 -0
  146. package/dist/organizations/events.d.ts +320 -181
  147. package/dist/organizations/events.js +90 -18
  148. package/dist/organizations/events.js.map +1 -1
  149. package/dist/organizations/index.d.ts +14 -10
  150. package/dist/organizations/index.js +7 -3
  151. package/dist/organizations/members.d.ts +66 -8
  152. package/dist/organizations/members.js.map +1 -1
  153. package/dist/organizations/notification-catalog.d.ts +31 -0
  154. package/dist/organizations/notification-catalog.js +32 -0
  155. package/dist/organizations/notification-catalog.js.map +1 -0
  156. package/dist/organizations/notification-categories.d.ts +6 -4
  157. package/dist/organizations/notification-categories.js +25 -10
  158. package/dist/organizations/notification-categories.js.map +1 -1
  159. package/dist/organizations/org-roles.d.ts +33 -0
  160. package/dist/organizations/org-roles.js +49 -0
  161. package/dist/organizations/org-roles.js.map +1 -0
  162. package/dist/organizations/permissions.d.ts +311 -0
  163. package/dist/organizations/permissions.js +327 -0
  164. package/dist/organizations/permissions.js.map +1 -0
  165. package/dist/organizations/ports.d.ts +184 -65
  166. package/dist/organizations/service.d.ts +102 -35
  167. package/dist/organizations/service.js +1245 -328
  168. package/dist/organizations/service.js.map +1 -1
  169. package/dist/organizations/testing.d.ts +18 -0
  170. package/dist/organizations/testing.js +37 -0
  171. package/dist/organizations/testing.js.map +1 -0
  172. package/dist/organizations/types.d.ts +5 -16
  173. package/dist/progress/events.d.ts +14 -14
  174. package/dist/progress/index.d.ts +2 -2
  175. package/dist/progress/ports.d.ts +11 -3
  176. package/dist/progress/service.d.ts +6 -5
  177. package/dist/progress/service.js +23 -8
  178. package/dist/progress/service.js.map +1 -1
  179. package/dist/reporting/admin/index.d.ts +6 -4
  180. package/dist/reporting/admin/model.d.ts +36 -3
  181. package/dist/reporting/admin/ports.d.ts +39 -14
  182. package/dist/reporting/admin/service.d.ts +32 -11
  183. package/dist/reporting/admin/service.js +102 -18
  184. package/dist/reporting/admin/service.js.map +1 -1
  185. package/dist/reporting/events/index.d.ts +27 -0
  186. package/dist/reporting/events/index.js +20 -0
  187. package/dist/reporting/events/index.js.map +1 -0
  188. package/dist/reporting/member/calendar.d.ts +11 -0
  189. package/dist/reporting/member/calendar.js +75 -0
  190. package/dist/reporting/member/calendar.js.map +1 -0
  191. package/dist/reporting/member/index.d.ts +3 -2
  192. package/dist/reporting/member/model.d.ts +26 -5
  193. package/dist/reporting/member/ports.d.ts +48 -29
  194. package/dist/reporting/member/service.d.ts +56 -29
  195. package/dist/reporting/member/service.js +328 -113
  196. package/dist/reporting/member/service.js.map +1 -1
  197. package/dist/shared/actor.d.ts +22 -3
  198. package/dist/shared/errors.d.ts +2 -1
  199. package/dist/shared/errors.js +3 -1
  200. package/dist/shared/errors.js.map +1 -1
  201. package/dist/shared/id.d.ts +4 -0
  202. package/dist/shared/id.js +4 -0
  203. package/dist/shared/id.js.map +1 -1
  204. package/dist/shared/logger.d.ts +2 -1
  205. package/dist/shared/logger.js +10 -1
  206. package/dist/shared/logger.js.map +1 -1
  207. package/dist/shared/ports.d.ts +25 -5
  208. package/dist/types/content.d.ts +2 -2
  209. package/dist/types/discussion.d.ts +4 -5
  210. package/dist/types/email-templates.d.ts +22 -3
  211. package/dist/types/entitlements.d.ts +2 -2
  212. package/dist/types/identity.d.ts +13 -16
  213. package/dist/types/index.d.ts +9 -9
  214. package/dist/types/index.js +2 -1
  215. package/dist/types/notifications.d.ts +4 -4
  216. package/dist/types/organizations.d.ts +3 -2
  217. package/dist/types/ports.d.ts +8 -1
  218. package/dist/types/ports.js +7 -1
  219. package/dist/types/ports.js.map +1 -0
  220. package/dist/types/schemas/assets.d.ts +5 -5
  221. package/dist/types/schemas/assets.js +4 -3
  222. package/dist/types/schemas/assets.js.map +1 -1
  223. package/dist/types/schemas/content.d.ts +537 -13
  224. package/dist/types/schemas/content.js +201 -7
  225. package/dist/types/schemas/content.js.map +1 -1
  226. package/dist/types/schemas/discussion.d.ts +282 -59
  227. package/dist/types/schemas/discussion.js +77 -3
  228. package/dist/types/schemas/discussion.js.map +1 -1
  229. package/dist/types/schemas/entitlements.d.ts +14 -1
  230. package/dist/types/schemas/entitlements.js +11 -4
  231. package/dist/types/schemas/entitlements.js.map +1 -1
  232. package/dist/types/schemas/index.d.ts +5 -5
  233. package/dist/types/schemas/index.js +5 -5
  234. package/dist/types/schemas/notifications.d.ts +13 -12
  235. package/dist/types/schemas/notifications.js +3 -2
  236. package/dist/types/schemas/notifications.js.map +1 -1
  237. package/dist/types/schemas/organizations.d.ts +96 -60
  238. package/dist/types/schemas/organizations.js +78 -27
  239. package/dist/types/schemas/organizations.js.map +1 -1
  240. package/dist/types/schemas/progress.d.ts +3 -4
  241. package/dist/types/schemas/progress.js +0 -1
  242. package/dist/types/schemas/progress.js.map +1 -1
  243. package/package.json +1 -1
  244. package/dist/organizations/access-scopes.d.ts +0 -19
  245. package/dist/organizations/access-scopes.js +0 -89
  246. package/dist/organizations/access-scopes.js.map +0 -1
  247. package/dist/organizations/roles.d.ts +0 -19
  248. package/dist/organizations/roles.js +0 -82
  249. package/dist/organizations/roles.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live.js","names":[],"sources":["../../src/discussion/live.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { DomainEvent, EventBus, JsonValue, Logger, PubSub } from '../shared/ports.js';\nimport { describeError, noopLogger } from '../shared/logger.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { ChatLiveEvent } from '../types/index.js';\nimport { discussionEvents } from './events.js';\nimport type {\n ChatCreated,\n ChatMuted,\n ChatRead,\n ChatUpdated,\n MessageDeleted,\n MessageSent,\n MessageUpdated,\n} from './events.js';\nimport type { ChatLiveReads } from './ports.js';\n\nexport const CHAT_CHANNEL = 'discussion.chats';\nexport const TYPING = 'typing';\n\nconst HEARTBEAT_MS = 25_000;\nconst PUBLISH_TIMEOUT_MS = 2000;\nconst FLUSH_MS = 1000;\nconst CLOSE_TIMEOUT_MS = 2000;\nconst MAX_CHATS = 16;\nconst MAX_PENDING = 5000;\nconst STREAM_QUEUE = 256;\n\nconst SENT = discussionEvents.messageSent.type;\nconst UPDATED = discussionEvents.messageUpdated.type;\nconst DELETED = discussionEvents.messageDeleted.type;\nconst REACTIONS = discussionEvents.messageReactionsChanged.type;\nconst CREATED = discussionEvents.chatCreated.type;\nconst CHAT_UPDATED = discussionEvents.chatUpdated.type;\nconst ADDED = discussionEvents.chatParticipantAdded.type;\nconst READ = discussionEvents.chatRead.type;\nconst MUTED = discussionEvents.chatMuted.type;\n\nconst chatChangeSchema = z.object({\n event: z.string(),\n orgId: z.string(),\n communityId: z.string(),\n chatId: z.string(),\n messageId: z.string().optional(),\n orgUserId: z.string().optional(),\n members: z.array(z.string()).optional(),\n reaction: z.string().nullable().optional(),\n until: z.string().optional(),\n});\n\n/** One chat change on the `discussion.chats` channel. It never carries a body;\n * every instance reads current state itself. */\nexport type ChatChange = z.infer<typeof chatChangeSchema>;\n\n/** Publishes a change, logging and dropping it when the publish fails or has\n * not finished within 2 s. Never throws. */\nexport async function publishChange(\n pubsub: PubSub,\n change: ChatChange,\n logger: Logger,\n timeoutMs = PUBLISH_TIMEOUT_MS,\n): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n pubsub.publish(CHAT_CHANNEL, change as JsonValue),\n new Promise((_, reject) => {\n timer = setTimeout(() => reject(new Error('publish timed out')), timeoutMs);\n }),\n ]);\n } catch (error) {\n logger.warn('live publish failed', {\n orgId: change.orgId,\n chatId: change.chatId,\n event: change.event,\n error: describeError(error),\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction actorOf(event: DomainEvent): string[] {\n const actor = event.metadata?.actorId;\n return typeof actor === 'string' ? [actor] : [];\n}\n\nfunction union(...lists: string[][]): string[] {\n return [...new Set(lists.flat())].sort();\n}\n\nexport type ChatLiveForwarderParams = {\n pubsub: PubSub;\n logger?: Logger;\n timeoutMs?: number;\n};\n\n/** Publishes every chat change committed through the outbox to every instance. */\nexport class ChatLiveForwarder {\n private readonly pubsub: PubSub;\n private readonly logger: Logger;\n private readonly timeoutMs: number;\n\n constructor(params: ChatLiveForwarderParams) {\n this.pubsub = params.pubsub;\n this.logger = params.logger ?? noopLogger;\n this.timeoutMs = params.timeoutMs ?? PUBLISH_TIMEOUT_MS;\n }\n\n attach(bus: EventBus): void {\n const forward = (change: ChatChange) =>\n publishChange(this.pubsub, change, this.logger, this.timeoutMs);\n const chat = (event: ChatCreated | ChatUpdated) =>\n forward({\n event: event.type,\n orgId: event.orgId,\n communityId: event.data.communityId,\n chatId: event.data.id,\n members: union(event.data.participantIds, actorOf(event)),\n });\n const message = (event: MessageSent | MessageUpdated | MessageDeleted) =>\n forward({\n event: event.type,\n orgId: event.orgId,\n communityId: event.data.communityId,\n chatId: event.data.chatId,\n messageId: event.data.id,\n });\n const member = (event: ChatRead | ChatMuted) =>\n forward({\n event: event.type,\n orgId: event.orgId,\n communityId: event.data.communityId,\n chatId: event.data.chatId,\n orgUserId: event.data.orgUserId,\n });\n bus.subscribe(discussionEvents.chatCreated, chat);\n bus.subscribe(discussionEvents.chatUpdated, chat);\n bus.subscribe(discussionEvents.chatParticipantAdded, (event) =>\n forward({\n event: event.type,\n orgId: event.orgId,\n communityId: event.data.communityId,\n chatId: event.data.chatId,\n orgUserId: event.data.orgUserId,\n members: union([event.data.orgUserId], actorOf(event)),\n }),\n );\n bus.subscribe(discussionEvents.messageSent, message);\n bus.subscribe(discussionEvents.messageUpdated, message);\n bus.subscribe(discussionEvents.messageDeleted, message);\n bus.subscribe(discussionEvents.messageReactionsChanged, (event) =>\n forward({\n event: event.type,\n orgId: event.orgId,\n communityId: event.data.communityId,\n chatId: event.data.chatId,\n messageId: event.data.messageId,\n orgUserId: event.data.orgUserId,\n reaction: event.data.reaction,\n }),\n );\n bus.subscribe(discussionEvents.chatRead, member);\n bus.subscribe(discussionEvents.chatMuted, member);\n }\n}\n\nexport interface ChatHubOptions {\n flushMs?: number;\n closeTimeoutMs?: number;\n}\n\nexport type ChatHubParams = ChatHubOptions & {\n pubsub: PubSub;\n reads: ChatLiveReads;\n logger?: Logger;\n};\n\nexport interface LiveRegistration {\n communityId: string;\n actor: Actor;\n}\n\nclass LiveStream implements AsyncIterableIterator<ChatLiveEvent> {\n readonly done: Promise<void>;\n private queue: ChatLiveEvent[] = [];\n private waiter: ((result: IteratorResult<ChatLiveEvent>) => void) | null = null;\n private ending = false;\n private finished = false;\n private resolveDone!: () => void;\n\n constructor(\n readonly orgId: string,\n readonly communityId: string,\n readonly orgUserId: string,\n private readonly onEnd: (stream: LiveStream) => void,\n private readonly onOverflow: (stream: LiveStream) => void,\n ) {\n this.done = new Promise((resolve) => {\n this.resolveDone = resolve;\n });\n }\n\n get ended(): boolean {\n return this.ending;\n }\n\n push(event: ChatLiveEvent): void {\n if (this.ending) {\n return;\n }\n const waiter = this.waiter;\n if (waiter) {\n this.waiter = null;\n waiter({ value: event, done: false });\n return;\n }\n if (this.queue.length >= STREAM_QUEUE) {\n this.queue = [{ type: 'resync' }];\n this.onOverflow(this);\n return;\n }\n this.queue.push(event);\n }\n\n end(last?: ChatLiveEvent): void {\n if (this.ending) {\n return;\n }\n if (last) {\n this.push(last);\n }\n this.ending = true;\n this.onEnd(this);\n const waiter = this.waiter;\n if (waiter && this.queue.length === 0) {\n this.waiter = null;\n this.finish();\n waiter({ value: undefined, done: true });\n }\n }\n\n next(): Promise<IteratorResult<ChatLiveEvent>> {\n const event = this.queue.shift();\n if (event) {\n return Promise.resolve({ value: event, done: false });\n }\n if (this.ending) {\n this.finish();\n return Promise.resolve({ value: undefined, done: true });\n }\n return new Promise((resolve) => {\n this.waiter = resolve;\n });\n }\n\n return(): Promise<IteratorResult<ChatLiveEvent>> {\n this.queue = [];\n this.end();\n const waiter = this.waiter;\n if (waiter) {\n this.waiter = null;\n waiter({ value: undefined, done: true });\n }\n this.finish();\n return Promise.resolve({ value: undefined, done: true });\n }\n\n [Symbol.asyncIterator](): AsyncIterableIterator<ChatLiveEvent> {\n return this;\n }\n\n private finish(): void {\n if (this.finished) {\n return;\n }\n this.finished = true;\n this.resolveDone();\n }\n}\n\ninterface ChatQueue {\n pending: ChatChange[];\n running: boolean;\n queued: boolean;\n}\n\ninterface DirtyGroup {\n orgId: string;\n communityId: string;\n members: Map<string, Set<string>>;\n}\n\nfunction coalesceKey(change: ChatChange): string | null {\n switch (change.event) {\n case SENT:\n case UPDATED:\n case DELETED:\n return `message:${change.messageId}`;\n case REACTIONS:\n return `reaction:${change.messageId}:${change.orgUserId}`;\n case CREATED:\n case CHAT_UPDATED:\n case ADDED:\n return 'chat';\n case READ:\n case MUTED:\n return `member:${change.orgUserId}`;\n default:\n return null;\n }\n}\n\nfunction merge(existing: ChatChange, next: ChatChange): ChatChange {\n switch (existing.event) {\n case SENT:\n case UPDATED:\n case DELETED: {\n const events = [existing.event, next.event];\n const event = events.includes(SENT) ? SENT : events.includes(DELETED) ? DELETED : UPDATED;\n return { ...next, event };\n }\n case CREATED:\n case CHAT_UPDATED:\n case ADDED:\n return { ...existing, members: union(existing.members ?? [], next.members ?? []) };\n case REACTIONS:\n return next;\n default:\n return existing;\n }\n}\n\nconst key = (orgId: string, id: string) => `${orgId}\\u0000${id}`;\n\n/** This process's open chat streams, and the live delivery of every chat\n * change to them. */\nexport class ChatHub {\n private readonly pubsub: PubSub;\n private readonly reads: ChatLiveReads;\n private readonly logger: Logger;\n private readonly flushMs: number;\n private readonly closeTimeoutMs: number;\n private readonly streams = new Set<LiveStream>();\n private readonly byMember = new Map<string, Set<LiveStream>>();\n private readonly byCommunity = new Map<string, Set<LiveStream>>();\n private readonly queues = new Map<string, ChatQueue>();\n private ready: string[] = [];\n private running = 0;\n private pending = 0;\n private dirty = new Map<string, DirtyGroup>();\n private flushing = false;\n private flushAgain = false;\n private closing = false;\n private unsubscribe: (() => void) | null = null;\n private flushTimer: NodeJS.Timeout | null = null;\n\n constructor(params: ChatHubParams) {\n this.pubsub = params.pubsub;\n this.reads = params.reads;\n this.logger = params.logger ?? noopLogger;\n this.flushMs = params.flushMs ?? FLUSH_MS;\n this.closeTimeoutMs = params.closeTimeoutMs ?? CLOSE_TIMEOUT_MS;\n }\n\n async start(): Promise<void> {\n if (this.unsubscribe || this.closing) {\n return;\n }\n this.unsubscribe = this.pubsub.subscribe(\n CHAT_CHANNEL,\n (message) => this.receive(message),\n () => this.reset(),\n );\n this.flushTimer = setInterval(() => {\n if (!this.flushing) {\n void this.flush();\n }\n }, this.flushMs);\n this.flushTimer.unref();\n this.logger.info('chat hub started');\n }\n\n /** Ends every stream with `closed { reason: 'shutdown' }` and waits, up to\n * 2 s in all, for each to be read to its end. */\n async close(): Promise<void> {\n if (this.closing) {\n return;\n }\n this.closing = true;\n this.unsubscribe?.();\n this.unsubscribe = null;\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n }\n this.queues.clear();\n this.ready = [];\n this.pending = 0;\n const streams = [...this.streams];\n for (const stream of streams) {\n stream.end({ type: 'closed', reason: 'shutdown' });\n }\n let timer: NodeJS.Timeout | undefined;\n await Promise.race([\n Promise.all(streams.map((stream) => stream.done)),\n new Promise((resolve) => {\n timer = setTimeout(resolve, this.closeTimeoutMs);\n }),\n ]);\n clearTimeout(timer);\n this.logger.info('chat hub closed', { streams: streams.length });\n }\n\n /** Registers one stream. It starts with `ready`, then the member's unread. */\n open(registration: LiveRegistration): AsyncIterable<ChatLiveEvent> {\n const { actor, communityId } = registration;\n const stream = new LiveStream(\n actor.orgId,\n communityId,\n actor.id,\n (ended) => this.unregister(ended),\n (full) => this.markDirty(full.orgId, full.communityId, full.orgUserId),\n );\n if (this.closing) {\n stream.end({ type: 'closed', reason: 'shutdown' });\n return stream;\n }\n this.streams.add(stream);\n this.index(this.byMember, key(stream.orgId, stream.orgUserId), stream);\n this.index(this.byCommunity, key(stream.orgId, stream.communityId), stream);\n stream.push({ type: 'ready', heartbeatMs: HEARTBEAT_MS });\n this.markDirty(stream.orgId, stream.communityId, stream.orgUserId);\n void this.flush();\n this.logger.debug('chat stream opened', {\n orgId: stream.orgId,\n communityId,\n streams: this.streams.size,\n });\n return stream;\n }\n\n private index(map: Map<string, Set<LiveStream>>, at: string, stream: LiveStream): void {\n const set = map.get(at) ?? new Set<LiveStream>();\n set.add(stream);\n map.set(at, set);\n }\n\n private unindex(map: Map<string, Set<LiveStream>>, at: string, stream: LiveStream): void {\n const set = map.get(at);\n set?.delete(stream);\n if (set?.size === 0) {\n map.delete(at);\n }\n }\n\n private unregister(stream: LiveStream): void {\n if (!this.streams.delete(stream)) {\n return;\n }\n this.unindex(this.byMember, key(stream.orgId, stream.orgUserId), stream);\n this.unindex(this.byCommunity, key(stream.orgId, stream.communityId), stream);\n this.logger.debug('chat stream closed', {\n orgId: stream.orgId,\n communityId: stream.communityId,\n streams: this.streams.size,\n });\n }\n\n private streamsOf(orgId: string, communityId: string, orgUserId: string): LiveStream[] {\n return [...(this.byMember.get(key(orgId, orgUserId)) ?? [])].filter(\n (stream) => stream.communityId === communityId,\n );\n }\n\n private communityStreams(orgId: string, communityId: string): LiveStream[] {\n return [...(this.byCommunity.get(key(orgId, communityId)) ?? [])];\n }\n\n private receive(message: JsonValue): void {\n const parsed = chatChangeSchema.safeParse(message);\n if (!parsed.success) {\n this.logger.warn('chat change unreadable');\n return;\n }\n const change = parsed.data;\n if (this.closing || !this.byCommunity.has(key(change.orgId, change.communityId))) {\n return;\n }\n const at = key(change.orgId, change.chatId);\n const queue = this.queues.get(at) ?? { pending: [], running: false, queued: false };\n this.queues.set(at, queue);\n const coalesce = coalesceKey(change);\n const index =\n coalesce === null ? -1 : queue.pending.findIndex((other) => coalesceKey(other) === coalesce);\n if (index >= 0) {\n queue.pending[index] = merge(queue.pending[index]!, change);\n } else {\n if (this.pending >= MAX_PENDING) {\n this.overflow();\n return;\n }\n queue.pending.push(change);\n this.pending += 1;\n }\n if (!queue.running && !queue.queued) {\n queue.queued = true;\n this.ready.push(at);\n }\n this.pump();\n }\n\n private pump(): void {\n while (this.running < MAX_CHATS && this.ready.length > 0) {\n const at = this.ready.shift()!;\n const queue = this.queues.get(at);\n if (!queue) {\n continue;\n }\n queue.queued = false;\n const change = queue.pending.shift();\n if (!change) {\n this.queues.delete(at);\n continue;\n }\n this.pending -= 1;\n queue.running = true;\n this.running += 1;\n void this.handle(change).finally(() => {\n queue.running = false;\n this.running -= 1;\n if (this.queues.get(at) === queue) {\n if (queue.pending.length > 0) {\n queue.queued = true;\n this.ready.push(at);\n } else {\n this.queues.delete(at);\n }\n }\n this.pump();\n });\n }\n }\n\n private overflow(): void {\n this.logger.warn('chat hub queue full; dropping pending changes', { pending: this.pending });\n for (const [at, queue] of this.queues) {\n queue.pending = [];\n queue.queued = false;\n if (!queue.running) {\n this.queues.delete(at);\n }\n }\n this.ready = [];\n this.pending = 0;\n this.resync([...this.streams]);\n }\n\n private reset(): void {\n this.logger.info('chat hub reset', { streams: this.streams.size });\n this.resync([...this.streams]);\n }\n\n private resync(streams: LiveStream[]): void {\n for (const stream of streams) {\n stream.push({ type: 'resync' });\n this.markDirty(stream.orgId, stream.communityId, stream.orgUserId);\n }\n }\n\n private async handle(change: ChatChange): Promise<void> {\n try {\n switch (change.event) {\n case SENT:\n case UPDATED:\n case DELETED:\n await this.onMessage(change);\n return;\n case REACTIONS:\n await this.onReactions(change);\n return;\n case CREATED:\n case CHAT_UPDATED:\n case ADDED:\n await this.onChat(change);\n return;\n case READ:\n case MUTED:\n if (change.orgUserId) {\n this.markDirty(change.orgId, change.communityId, change.orgUserId, change.chatId);\n }\n return;\n case TYPING:\n await this.onTyping(change);\n return;\n default:\n this.logger.debug('chat change ignored', { event: change.event });\n }\n } catch (error) {\n this.logger.error('chat change read failed', {\n orgId: change.orgId,\n chatId: change.chatId,\n event: change.event,\n error: describeError(error),\n });\n this.resync(this.communityStreams(change.orgId, change.communityId));\n }\n }\n\n private async participantStreams(\n change: ChatChange,\n ): Promise<{ members: string[]; streams: LiveStream[] }> {\n const active = await this.reads.activeParticipants(change.orgId, change.chatId);\n const members: string[] = [];\n const streams: LiveStream[] = [];\n for (const orgUserId of active) {\n const local = this.streamsOf(change.orgId, change.communityId, orgUserId);\n if (local.length > 0) {\n members.push(orgUserId);\n streams.push(...local);\n }\n }\n return { members, streams };\n }\n\n private async onMessage(change: ChatChange): Promise<void> {\n const { members, streams } = await this.participantStreams(change);\n if (streams.length === 0 || !change.messageId) {\n return;\n }\n const [view] = await this.reads.messageViews(change.orgId, [change.messageId]);\n if (!view || view.chatId !== change.chatId) {\n return;\n }\n const event: ChatLiveEvent = view.deletedAt\n ? {\n type: 'message.deleted',\n chatId: change.chatId,\n messageId: view.id,\n deletedAt: view.deletedAt,\n }\n : {\n type: change.event === SENT ? 'message.created' : 'message.updated',\n chatId: change.chatId,\n message: view,\n };\n for (const stream of streams) {\n stream.push(event);\n }\n if (change.event !== UPDATED || view.deletedAt) {\n for (const orgUserId of members) {\n this.markDirty(change.orgId, change.communityId, orgUserId, change.chatId);\n }\n }\n }\n\n private async onReactions(change: ChatChange): Promise<void> {\n const { streams } = await this.participantStreams(change);\n if (streams.length === 0 || !change.messageId || !change.orgUserId) {\n return;\n }\n const reactions = await this.reads.messageReactions(change.orgId, change.messageId);\n for (const stream of streams) {\n stream.push({\n type: 'message.reactions',\n chatId: change.chatId,\n messageId: change.messageId,\n reactions,\n orgUserId: change.orgUserId,\n reaction: change.reaction ?? null,\n });\n }\n }\n\n private async onChat(change: ChatChange): Promise<void> {\n const live = await this.reads.liveChat(change.orgId, change.chatId);\n const active = new Set(live?.participantIds ?? []);\n if (live) {\n for (const orgUserId of active) {\n const streams = this.streamsOf(change.orgId, change.communityId, orgUserId);\n if (streams.length === 0) {\n continue;\n }\n for (const stream of streams) {\n stream.push({\n type: 'chat.updated',\n chatId: change.chatId,\n chat: live.chat,\n participants: live.participants,\n });\n }\n this.markDirty(change.orgId, change.communityId, orgUserId, change.chatId);\n }\n }\n for (const orgUserId of change.members ?? []) {\n if (active.has(orgUserId)) {\n continue;\n }\n const streams = this.streamsOf(change.orgId, change.communityId, orgUserId);\n if (streams.length === 0) {\n continue;\n }\n for (const stream of streams) {\n stream.push({ type: 'chat.removed', chatId: change.chatId });\n }\n this.markDirty(change.orgId, change.communityId, orgUserId);\n }\n }\n\n private async onTyping(change: ChatChange): Promise<void> {\n if (!change.orgUserId || !change.until) {\n return;\n }\n const { streams } = await this.participantStreams(change);\n const until = new Date(change.until);\n for (const stream of streams) {\n if (stream.orgUserId === change.orgUserId) {\n continue;\n }\n stream.push({\n type: 'typing',\n chatId: change.chatId,\n orgUserId: change.orgUserId,\n until,\n });\n }\n }\n\n private markDirty(orgId: string, communityId: string, orgUserId: string, chatId?: string): void {\n const at = key(orgId, communityId);\n const group = this.dirty.get(at) ?? { orgId, communityId, members: new Map() };\n this.dirty.set(at, group);\n const chats = group.members.get(orgUserId) ?? new Set<string>();\n group.members.set(orgUserId, chats);\n if (chatId) {\n chats.add(chatId);\n }\n }\n\n /** Pushes the `chat.state` of every dirty member and chat and the `unread`\n * of every dirty member, read now. One flush runs at a time. */\n private async flush(): Promise<void> {\n if (this.flushing) {\n this.flushAgain = true;\n return;\n }\n this.flushing = true;\n try {\n do {\n this.flushAgain = false;\n const groups = [...this.dirty.values()];\n this.dirty = new Map();\n await Promise.all(groups.map((group) => this.flushGroup(group)));\n } while (this.flushAgain && this.dirty.size > 0 && !this.closing);\n } finally {\n this.flushing = false;\n }\n }\n\n private async flushGroup(group: DirtyGroup): Promise<void> {\n const { orgId, communityId } = group;\n const members = [...group.members.keys()].filter(\n (orgUserId) => this.streamsOf(orgId, communityId, orgUserId).length > 0,\n );\n if (members.length === 0) {\n return;\n }\n let states: Awaited<ReturnType<ChatLiveReads['memberStates']>>;\n try {\n states = await this.reads.memberStates(orgId, communityId, members);\n } catch (error) {\n this.logger.error('chat member state read failed', {\n orgId,\n communityId,\n error: describeError(error),\n });\n for (const orgUserId of members) {\n const chats = group.members.get(orgUserId) ?? new Set<string>();\n this.markDirty(orgId, communityId, orgUserId);\n for (const chatId of chats) {\n this.markDirty(orgId, communityId, orgUserId, chatId);\n }\n }\n return;\n }\n for (const state of states) {\n const chats = group.members.get(state.orgUserId);\n const streams = this.streamsOf(orgId, communityId, state.orgUserId);\n if (!chats || streams.length === 0) {\n continue;\n }\n const events: ChatLiveEvent[] = state.chats\n .filter((chat) => chats.has(chat.chatId))\n .map((chat) => ({\n type: 'chat.state',\n chatId: chat.chatId,\n unread: chat.unread,\n lastReadMessageId: chat.lastReadMessageId,\n subscription: chat.subscription,\n }));\n events.push({ type: 'unread', ...state.unread });\n for (const stream of streams) {\n for (const event of events) {\n stream.push(event);\n }\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { authorView } from "./authors.js";
1
+ import { authorName } from "./authors.js";
2
2
  //#region src/discussion/mentions.ts
3
3
  const TOKEN = /<@([A-Za-z0-9_-]+)>/g;
4
4
  const UNNAMED = "A member";
@@ -25,7 +25,7 @@ async function mentionViews(mentions, profiles, orgId, itemType, itemIds) {
25
25
  const record = records[orgUserId];
26
26
  return record ? [{
27
27
  orgUserId,
28
- name: authorView(orgId, record, null).displayName || UNNAMED
28
+ name: authorName(record) || UNNAMED
29
29
  }] : [];
30
30
  })]));
31
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mentions.js","names":[],"sources":["../../src/discussion/mentions.ts"],"sourcesContent":["import type { MentionItemType, MentionView } from '../types/index.js';\nimport { authorView } from './authors.js';\nimport type { AuthorProfileReader, MentionsRepository } from './ports.js';\n\nconst TOKEN = /<@([A-Za-z0-9_-]+)>/g;\nconst UNNAMED = 'A member';\n\nexport function mentionTokens(...texts: string[]): string[] {\n const found = new Set<string>();\n for (const text of texts) {\n for (const match of text.matchAll(TOKEN)) {\n found.add(match[1]!);\n }\n }\n return [...found];\n}\n\nexport async function nameMentions(\n text: string,\n nameOf: (orgUserId: string) => Promise<string>,\n): Promise<string> {\n const names = new Map<string, string>();\n for (const orgUserId of mentionTokens(text)) {\n names.set(orgUserId, await nameOf(orgUserId));\n }\n return text.replace(TOKEN, (_token, orgUserId: string) => names.get(orgUserId) ?? UNNAMED);\n}\n\nexport function keepAllowed(tokens: string[], allowed: string[]): string[] {\n const kept = new Set(allowed);\n return tokens.filter((token) => kept.has(token));\n}\n\nexport async function mentionViews(\n mentions: Pick<MentionsRepository, 'forItems'>,\n profiles: AuthorProfileReader,\n orgId: string,\n itemType: MentionItemType,\n itemIds: string[],\n): Promise<Record<string, MentionView[]>> {\n if (itemIds.length === 0) {\n return {};\n }\n const byItem = await mentions.forItems(orgId, itemType, [...new Set(itemIds)]);\n const people = [...new Set(Object.values(byItem).flat())];\n const records = people.length > 0 ? await profiles.authorsOf(orgId, people) : {};\n return Object.fromEntries(\n Object.entries(byItem).map(([itemId, orgUserIds]) => [\n itemId,\n orgUserIds.flatMap((orgUserId) => {\n const record = records[orgUserId];\n return record\n ? [{ orgUserId, name: authorView(orgId, record, null).displayName || UNNAMED }]\n : [];\n }),\n ]),\n );\n}\n"],"mappings":";;AAIA,MAAM,QAAQ;AACd,MAAM,UAAU;AAEhB,SAAgB,cAAc,GAAG,OAA2B;CAC1D,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,QAAQ,OACjB,KAAK,MAAM,SAAS,KAAK,SAAS,KAAK,GACrC,MAAM,IAAI,MAAM,EAAG;CAGvB,OAAO,CAAC,GAAG,KAAK;AAClB;AAEA,eAAsB,aACpB,MACA,QACiB;CACjB,MAAM,wBAAQ,IAAI,IAAoB;CACtC,KAAK,MAAM,aAAa,cAAc,IAAI,GACxC,MAAM,IAAI,WAAW,MAAM,OAAO,SAAS,CAAC;CAE9C,OAAO,KAAK,QAAQ,QAAQ,QAAQ,cAAsB,MAAM,IAAI,SAAS,KAAK,OAAO;AAC3F;AAEA,SAAgB,YAAY,QAAkB,SAA6B;CACzE,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,OAAO,QAAQ,UAAU,KAAK,IAAI,KAAK,CAAC;AACjD;AAEA,eAAsB,aACpB,UACA,UACA,OACA,UACA,SACwC;CACxC,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;CAEV,MAAM,SAAS,MAAM,SAAS,SAAS,OAAO,UAAU,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC,CAAC;CAC7E,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,OAAO,OAAO,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;CACxD,MAAM,UAAU,OAAO,SAAS,IAAI,MAAM,SAAS,UAAU,OAAO,MAAM,IAAI,CAAC;CAC/E,OAAO,OAAO,YACZ,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,CAAC,QAAQ,gBAAgB,CACnD,QACA,WAAW,SAAS,cAAc;EAChC,MAAM,SAAS,QAAQ;EACvB,OAAO,SACH,CAAC;GAAE;GAAW,MAAM,WAAW,OAAO,QAAQ,IAAI,CAAC,CAAC,eAAe;EAAQ,CAAC,IAC5E,CAAC;CACP,CAAC,CACH,CAAC,CACH;AACF"}
1
+ {"version":3,"file":"mentions.js","names":[],"sources":["../../src/discussion/mentions.ts"],"sourcesContent":["import type { MentionItemType, MentionView } from '../types/index.js';\nimport { authorName } from './authors.js';\nimport type { AuthorProfileReader, MentionsRepository } from './ports.js';\n\nconst TOKEN = /<@([A-Za-z0-9_-]+)>/g;\nconst UNNAMED = 'A member';\n\nexport function mentionTokens(...texts: string[]): string[] {\n const found = new Set<string>();\n for (const text of texts) {\n for (const match of text.matchAll(TOKEN)) {\n found.add(match[1]!);\n }\n }\n return [...found];\n}\n\nexport async function nameMentions(\n text: string,\n nameOf: (orgUserId: string) => Promise<string>,\n): Promise<string> {\n const names = new Map<string, string>();\n for (const orgUserId of mentionTokens(text)) {\n names.set(orgUserId, await nameOf(orgUserId));\n }\n return text.replace(TOKEN, (_token, orgUserId: string) => names.get(orgUserId) ?? UNNAMED);\n}\n\nexport function keepAllowed(tokens: string[], allowed: string[]): string[] {\n const kept = new Set(allowed);\n return tokens.filter((token) => kept.has(token));\n}\n\nexport async function mentionViews(\n mentions: Pick<MentionsRepository, 'forItems'>,\n profiles: AuthorProfileReader,\n orgId: string,\n itemType: MentionItemType,\n itemIds: string[],\n): Promise<Record<string, MentionView[]>> {\n if (itemIds.length === 0) {\n return {};\n }\n const byItem = await mentions.forItems(orgId, itemType, [...new Set(itemIds)]);\n const people = [...new Set(Object.values(byItem).flat())];\n const records = people.length > 0 ? await profiles.authorsOf(orgId, people) : {};\n return Object.fromEntries(\n Object.entries(byItem).map(([itemId, orgUserIds]) => [\n itemId,\n orgUserIds.flatMap((orgUserId) => {\n const record = records[orgUserId];\n return record ? [{ orgUserId, name: authorName(record) || UNNAMED }] : [];\n }),\n ]),\n );\n}\n"],"mappings":";;AAIA,MAAM,QAAQ;AACd,MAAM,UAAU;AAEhB,SAAgB,cAAc,GAAG,OAA2B;CAC1D,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,QAAQ,OACjB,KAAK,MAAM,SAAS,KAAK,SAAS,KAAK,GACrC,MAAM,IAAI,MAAM,EAAG;CAGvB,OAAO,CAAC,GAAG,KAAK;AAClB;AAEA,eAAsB,aACpB,MACA,QACiB;CACjB,MAAM,wBAAQ,IAAI,IAAoB;CACtC,KAAK,MAAM,aAAa,cAAc,IAAI,GACxC,MAAM,IAAI,WAAW,MAAM,OAAO,SAAS,CAAC;CAE9C,OAAO,KAAK,QAAQ,QAAQ,QAAQ,cAAsB,MAAM,IAAI,SAAS,KAAK,OAAO;AAC3F;AAEA,SAAgB,YAAY,QAAkB,SAA6B;CACzE,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,OAAO,QAAQ,UAAU,KAAK,IAAI,KAAK,CAAC;AACjD;AAEA,eAAsB,aACpB,UACA,UACA,OACA,UACA,SACwC;CACxC,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;CAEV,MAAM,SAAS,MAAM,SAAS,SAAS,OAAO,UAAU,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC,CAAC;CAC7E,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,OAAO,OAAO,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;CACxD,MAAM,UAAU,OAAO,SAAS,IAAI,MAAM,SAAS,UAAU,OAAO,MAAM,IAAI,CAAC;CAC/E,OAAO,OAAO,YACZ,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,CAAC,QAAQ,gBAAgB,CACnD,QACA,WAAW,SAAS,cAAc;EAChC,MAAM,SAAS,QAAQ;EACvB,OAAO,SAAS,CAAC;GAAE;GAAW,MAAM,WAAW,MAAM,KAAK;EAAQ,CAAC,IAAI,CAAC;CAC1E,CAAC,CACH,CAAC,CACH;AACF"}
@@ -1,15 +1,13 @@
1
1
  import { JsonValue, Page } from "../types/shared.js";
2
- import { Activity, SpaceDetail } from "../types/schemas/content.js";
3
- import { Role } from "../types/schemas/organizations.js";
4
- import { Chat, ChatLastMessage, ChatUser, CommentListItem, CommentView, CommentsConfig, DigestRun, FlaggedComment, ListCommentsQuery, MentionItemType, Message, MessageFlag, MessageFlagResolution, MessageView, Post, PostFlag, PostFlagResolution, PostStatus, PostView, ReactionCounts, ReactionEmoji, Subscription, SubscriptionItemType, SubscriptionView } from "../types/schemas/discussion.js";
5
- import { Comment, CommentReport } from "../types/discussion.js";
2
+ import { Activity, DmSettings, SpaceDetail } from "../types/schemas/content.js";
3
+ import { Chat, ChatLastMessage, ChatLiveEvent, ChatUnreadSummary, ChatUser, CommentListItem, CommentView, CommentsConfig, DigestRun, FlaggedComment, FlaggedCommentView, ListCommentsQuery, LiveMessage, MentionItemType, Message, MessageFlag, MessageFlagResolution, MessageView, Post, PostFlag, PostFlagResolution, PostStatus, PostView, ReactionCounts, ReactionEmoji, Subscription, SubscriptionItemType, SubscriptionView } from "../types/schemas/discussion.js";
4
+ import { Comment, CommentAuthor, CommentReport } from "../types/discussion.js";
6
5
  import { SubjectRef } from "../types/notifications.js";
7
6
  import "../types/index.js";
7
+ import { Actor, EffectivePermissions } from "../shared/actor.js";
8
8
  import { OutboxAppender, UnitOfWork } from "../shared/ports.js";
9
- import { Actor } from "../shared/actor.js";
10
9
  import { CommunityDigest } from "../content/ports.js";
11
10
  import "../content/index.js";
12
- import "../organizations/index.js";
13
11
  import "./model.js";
14
12
  import "./types.js";
15
13
  //#region src/discussion/ports.d.ts
@@ -29,7 +27,6 @@ interface CommentWithContext {
29
27
  activityTitle: string;
30
28
  }
31
29
  type PostCommentInput = {
32
- actor: Actor;
33
30
  parentId: string | null;
34
31
  body: string;
35
32
  } & ({
@@ -40,14 +37,13 @@ type PostCommentInput = {
40
37
  activityId?: undefined;
41
38
  });
42
39
  /** A profile row as the repository loads it, keyed by `org_users.id`. `email`
43
- * is stripped before comments are served and kept only for the staff list. */
40
+ * is stripped before comments are served and kept only for the moderation list. */
44
41
  interface AuthorRecord {
45
42
  id: string;
46
43
  firstName: string | null;
47
44
  lastName: string | null;
48
45
  email: string;
49
46
  image: string | null;
50
- role: Role;
51
47
  displayName: string | null;
52
48
  headline: string | null;
53
49
  imageAssetOrgId: string | null;
@@ -58,43 +54,46 @@ interface ItemTitle {
58
54
  excerpt?: string;
59
55
  }
60
56
  interface ItemTitles {
61
- titlesFor(orgId: string, actor: Actor, refs: SubjectRef[]): Promise<Record<string, ItemTitle>>;
57
+ titlesFor(refs: SubjectRef[], actor: Actor): Promise<Record<string, ItemTitle>>;
62
58
  }
63
59
  interface AuthorProfileReader {
64
60
  authorsOf(orgId: string, orgUserIds: string[]): Promise<Record<string, AuthorRecord>>;
65
61
  }
62
+ /** Where a comment must be for a moderation call: in one community, on a
63
+ * lesson, or anywhere the actor can reach. */
64
+ type CommentWithin = {
65
+ communityId: string;
66
+ } | 'lesson' | 'any';
66
67
  interface DiscussionService {
67
68
  /** Post a root comment or a reply. Lands pending where review is required and
68
- * the poster is not staff. */
69
- postComment(orgId: string, input: PostCommentInput): Promise<CommentView>;
70
- /** Author-only. Throws ForbiddenError for anyone else, staff included. */
71
- edit(orgId: string, commentId: string, actor: Actor, body: string): Promise<CommentView>;
72
- /** The author, or anyone whose role is staff. */
73
- remove(orgId: string, commentId: string, actor: Actor, communityId?: string): Promise<CommentView>;
74
- /** Staff only. Returns the comment to published. */
75
- restore(orgId: string, commentId: string, actor: Actor, communityId?: string): Promise<CommentView>;
76
- /** Staff only. Publishes a pending comment. */
77
- approve(orgId: string, commentId: string, actor: Actor): Promise<Comment>;
78
- /** Make a comment published, whichever state it is in. Staff only. */
79
- publish(orgId: string, commentId: string, actor: Actor): Promise<Comment>;
69
+ * the poster does not skip review. */
70
+ postComment(input: PostCommentInput, actor: Actor): Promise<CommentView>;
71
+ /** Reply to a lesson comment as a moderator of its course; a reply to a reply
72
+ * attaches to its root. */
73
+ replyToComment(commentId: string, body: string, actor: Actor): Promise<CommentView>;
74
+ /** Author-only. Throws ForbiddenError for anyone else, moderators included. */
75
+ edit(commentId: string, body: string, actor: Actor): Promise<CommentView>;
76
+ /** The author, or a moderator of where the comment is. */
77
+ remove(commentId: string, within: CommentWithin, actor: Actor): Promise<CommentView>;
78
+ /** A moderator of where the comment is. Returns the comment to published. */
79
+ restore(commentId: string, within: CommentWithin, actor: Actor): Promise<CommentView>;
80
+ /** A moderator of where the comment is. Publishes a pending comment. */
81
+ approve(commentId: string, actor: Actor): Promise<Comment>;
82
+ /** Make a comment published, whichever state it is in. A moderator of where
83
+ * the comment is. */
84
+ publish(commentId: string, within: CommentWithin, actor: Actor): Promise<Comment>;
80
85
  /** The activity's comments as this reader may see them. */
81
- activityComments(orgId: string, activityId: string, actor: Actor): Promise<ActivityComments>;
86
+ activityComments(activityId: string, actor: Actor): Promise<ActivityComments>;
82
87
  /** One page of top-level comments, oldest first, each with its replies. */
83
- postComments(orgId: string, postId: string, actor: Actor, query: PageQuery): Promise<Page<CommentView>>;
84
- /**
85
- * The org's comments, filtered. Staff-scoped: rows carry the author's email
86
- * and a removed comment's body, neither of which a learner may see, so the
87
- * edge admits only staff.
88
- *
89
- * No rule is applied beyond the filters — this is what exists, not what
90
- * someone is allowed to read.
91
- */
92
- listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentListItem>>;
93
- setReaction(orgId: string, commentId: string, actor: Actor, emoji: ReactionEmoji | null): Promise<CommentReactions>;
94
- resolveReports(orgId: string, commentId: string, actor: Actor, communityId?: string): Promise<void>;
88
+ postComments(postId: string, query: PageQuery, actor: Actor): Promise<Page<CommentView>>;
89
+ /** The lesson comments of the courses the actor moderates, filtered. Rows
90
+ * carry the author's email and a removed comment's body. */
91
+ listComments(query: ListCommentsQuery, actor: Actor): Promise<Page<CommentListItem>>;
92
+ setReaction(commentId: string, emoji: ReactionEmoji | null, actor: Actor): Promise<CommentReactions>;
93
+ resolveReports(commentId: string, within: CommentWithin, actor: Actor): Promise<void>;
95
94
  /** Accepted even on locked comments — a locked activity can still hold
96
95
  * something a moderator needs to see. */
97
- reportComment(orgId: string, commentId: string, actor: Actor, reason: string): Promise<CommentReport>;
96
+ reportComment(commentId: string, reason: string, actor: Actor): Promise<CommentReport>;
98
97
  /** Plain read of one comment — no rules applied. */
99
98
  getComment(orgId: string, commentId: string): Promise<Comment | null>;
100
99
  commenterIds(orgId: string, target: {
@@ -107,7 +106,24 @@ interface ContentAccessReader {
107
106
  hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean>;
108
107
  }
109
108
  interface ActivityGetter {
110
- getActivity(orgId: string, activityId: string): Promise<Activity | null>;
109
+ getActivity(activityId: string, actor: Actor): Promise<Activity | null>;
110
+ }
111
+ interface SpaceReader {
112
+ getSpace(spaceId: string, actor: Actor): Promise<SpaceDetail | null>;
113
+ getDmSettings(communityId: string, actor: Actor): Promise<DmSettings | null>;
114
+ }
115
+ interface OpenableSpace {
116
+ id: string;
117
+ communityId: string;
118
+ accessRule: SpaceDetail['accessRule'];
119
+ }
120
+ interface CommunityAccess {
121
+ isCommunityMember(communityId: string, actor: Actor): Promise<boolean>;
122
+ canOpenSpace(spaceId: string, actor: Actor): Promise<boolean>;
123
+ canOpen(space: OpenableSpace, actor: Actor): Promise<boolean>;
124
+ openSpaceIds(communityId: string, actor: Actor): Promise<string[]>;
125
+ communityMembersAmong(orgId: string, communityId: string, orgUserIds: string[]): Promise<string[]>;
126
+ spaceOpenersAmong(orgId: string, spaceId: string, orgUserIds: string[]): Promise<string[]>;
111
127
  }
112
128
  interface DiscussionRepository extends PostCommentReader, AuthorProfileReader {
113
129
  insertComment(orgId: string, comment: Comment): Promise<Comment>;
@@ -133,7 +149,8 @@ interface DiscussionRepository extends PostCommentReader, AuthorProfileReader {
133
149
  resolvedBy: string;
134
150
  resolution: 'dismissed' | 'removed';
135
151
  }): Promise<void>;
136
- listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentWithContext>>;
152
+ /** `courseIds` limits the rows to those courses; undefined means every course. */
153
+ listComments(orgId: string, query: ListCommentsQuery, courseIds?: string[]): Promise<Page<CommentWithContext>>;
137
154
  }
138
155
  /** Writes that emit events run through this scope so the row and the outbox
139
156
  * entry commit in one transaction. */
@@ -143,9 +160,10 @@ interface DiscussionWriteScope {
143
160
  outbox: OutboxAppender;
144
161
  }
145
162
  type DiscussionUnitOfWork = UnitOfWork<DiscussionWriteScope>;
146
- interface AuthorRoleReader {
147
- roleOf(orgId: string, orgUserId: string): Promise<Role | null>;
148
- rolesOf(orgId: string, orgUserIds: string[]): Promise<Record<string, Role>>;
163
+ interface AuthorPermissionReader {
164
+ permissionsOfMany(orgId: string, orgUserIds: string[]): Promise<Map<string, EffectivePermissions>>;
165
+ actorFor(orgId: string, orgUserId: string): Promise<Actor | null>;
166
+ systemActor(orgId: string): Actor;
149
167
  }
150
168
  type PostMode = 'draft' | 'submit' | 'schedule';
151
169
  interface CreatePostInput {
@@ -188,7 +206,7 @@ declare const MODERATION_QUEUE_LIMIT = 100;
188
206
  interface SpaceModerationQueue {
189
207
  pending: PostView[];
190
208
  flags: PostFlag[];
191
- commentFlags: FlaggedComment[];
209
+ commentFlags: FlaggedCommentView[];
192
210
  }
193
211
  interface PostCommentReader {
194
212
  listOpenFlagsForSpace(orgId: string, spaceId: string, limit: number): Promise<FlaggedComment[]>;
@@ -202,40 +220,41 @@ interface PublishedPost {
202
220
  commentCount: number;
203
221
  }
204
222
  interface PostService {
205
- visibleTitles(orgId: string, actor: Actor, postIds: string[]): Promise<Record<string, string>>;
223
+ visibleTitles(postIds: string[], actor: Actor): Promise<Record<string, string>>;
206
224
  publishedBetween(orgId: string, communityId: string, from: Date, to: Date): Promise<PublishedPost[]>;
207
- create(orgId: string, actor: Actor, input: CreatePostInput): Promise<PostView>;
208
- update(orgId: string, postId: string, actor: Actor, patch: UpdatePostInput): Promise<PostView>;
209
- submit(orgId: string, postId: string, actor: Actor): Promise<PostView>;
210
- schedule(orgId: string, postId: string, actor: Actor, scheduledAt: Date): Promise<PostView>;
211
- unschedule(orgId: string, postId: string, actor: Actor): Promise<PostView>;
225
+ create(input: CreatePostInput, actor: Actor): Promise<PostView>;
226
+ update(postId: string, patch: UpdatePostInput, actor: Actor): Promise<PostView>;
227
+ submit(postId: string, actor: Actor): Promise<PostView>;
228
+ schedule(postId: string, scheduledAt: Date, actor: Actor): Promise<PostView>;
229
+ unschedule(postId: string, actor: Actor): Promise<PostView>;
212
230
  publishScheduled(orgId: string, postId: string, now: Date): Promise<void>;
213
- approve(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;
214
- remove(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;
215
- restore(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;
216
- pin(orgId: string, postId: string, actor: Actor): Promise<PostView>;
217
- unpin(orgId: string, postId: string, actor: Actor): Promise<PostView>;
218
- closeComments(orgId: string, postId: string, actor: Actor): Promise<PostView>;
219
- openComments(orgId: string, postId: string, actor: Actor): Promise<PostView>;
220
- setReactions(orgId: string, postId: string, actor: Actor, enabled: boolean): Promise<PostView>;
221
- setReaction(orgId: string, postId: string, actor: Actor, emoji: ReactionEmoji | null): Promise<PostReactions>;
222
- bookmark(orgId: string, postId: string, actor: Actor): Promise<void>;
223
- unbookmark(orgId: string, postId: string, actor: Actor): Promise<void>;
224
- flag(orgId: string, postId: string, actor: Actor, reason: string): Promise<PostFlag>;
225
- resolveFlags(orgId: string, postId: string, actor: Actor, resolution: Extract<PostFlagResolution, 'dismissed'>, communityId?: string): Promise<void>;
226
- spaceFeed(orgId: string, actor: Actor, communityId: string, spaceId: string, query: FeedQuery): Promise<Page<PostView>>;
227
- memberFeed(orgId: string, actor: Actor, communityId: string, query: Pick<FeedQuery, 'page' | 'pageSize'>): Promise<Page<PostView>>;
228
- listOwn(orgId: string, actor: Actor, communityId: string, query: PageQuery): Promise<Page<PostView>>;
229
- listBookmarked(orgId: string, actor: Actor, communityId: string, query: PageQuery): Promise<Page<PostView>>;
231
+ approve(postId: string, communityId: string | null, actor: Actor): Promise<PostView>;
232
+ remove(postId: string, communityId: string | null, actor: Actor): Promise<PostView>;
233
+ restore(postId: string, communityId: string | null, actor: Actor): Promise<PostView>;
234
+ pin(postId: string, actor: Actor): Promise<PostView>;
235
+ unpin(postId: string, actor: Actor): Promise<PostView>;
236
+ closeComments(postId: string, actor: Actor): Promise<PostView>;
237
+ openComments(postId: string, actor: Actor): Promise<PostView>;
238
+ setReactions(postId: string, enabled: boolean, actor: Actor): Promise<PostView>;
239
+ setReaction(postId: string, emoji: ReactionEmoji | null, actor: Actor): Promise<PostReactions>;
240
+ bookmark(postId: string, actor: Actor): Promise<void>;
241
+ unbookmark(postId: string, actor: Actor): Promise<void>;
242
+ flag(postId: string, reason: string, actor: Actor): Promise<PostFlag>;
243
+ resolveFlags(postId: string, resolution: Extract<PostFlagResolution, 'dismissed'>, communityId: string | null, actor: Actor): Promise<void>;
244
+ spaceFeed(communityId: string, spaceId: string, query: FeedQuery, actor: Actor): Promise<Page<PostView>>;
245
+ memberFeed(communityId: string, query: Pick<FeedQuery, 'page' | 'pageSize'>, actor: Actor): Promise<Page<PostView>>;
246
+ listOwn(communityId: string, query: PageQuery, actor: Actor): Promise<Page<PostView>>;
247
+ listBookmarked(communityId: string, query: PageQuery, actor: Actor): Promise<Page<PostView>>;
230
248
  /** Signed URL of the post's cover, for whoever can read the post. */
231
- coverFile(orgId: string, actor: Actor, postId: string): Promise<string | null>;
232
- getPost(orgId: string, actor: Actor, postId: string): Promise<PostView>;
233
- visible(orgId: string, actor: Actor, postId: string): Promise<{
249
+ coverFile(postId: string, actor: Actor): Promise<string>;
250
+ assetFile(postId: string, assetId: string, actor: Actor): Promise<string>;
251
+ getPost(postId: string, actor: Actor): Promise<PostView>;
252
+ visible(postId: string, actor: Actor): Promise<{
234
253
  post: Post;
235
254
  space: SpaceDetail;
236
255
  }>;
237
256
  find(orgId: string, postId: string): Promise<Post | null>;
238
- moderationQueue(orgId: string, actor: Actor, communityId: string, spaceId: string): Promise<SpaceModerationQueue>;
257
+ moderationQueue(communityId: string, spaceId: string, actor: Actor): Promise<SpaceModerationQueue>;
239
258
  }
240
259
  interface PostGuard {
241
260
  status: PostStatus;
@@ -302,16 +321,54 @@ interface SendMessageInput {
302
321
  body: string;
303
322
  replyToId: string | null;
304
323
  assetIds: string[];
324
+ clientId?: string;
305
325
  }
306
326
  interface ChatListItem {
307
327
  chat: Chat;
308
328
  participantIds: string[];
329
+ participants: CommentAuthor[];
309
330
  lastReadMessageId: string | null;
310
331
  subscription: SubscriptionView | null;
311
332
  unread: number;
312
333
  lastMessage: ChatLastMessage | null;
313
334
  }
314
- type ChatInboxRow = Omit<ChatListItem, 'subscription'>;
335
+ type ChatInboxRow = Omit<ChatListItem, 'subscription' | 'participants'>;
336
+ /** `requests`: true for only the pending requests to the member, false for
337
+ * everything else, omitted for every chat. */
338
+ interface ChatListQuery extends PageQuery {
339
+ requests?: boolean;
340
+ }
341
+ interface ReactionChange {
342
+ inserted: boolean;
343
+ changed: boolean;
344
+ }
345
+ interface ChatMemberState {
346
+ chatId: string;
347
+ unread: number;
348
+ lastReadMessageId: string | null;
349
+ subscription: SubscriptionView | null;
350
+ }
351
+ /** A member's state in each chat they are in in one community, with their
352
+ * unread total there. */
353
+ interface ChatMemberStates {
354
+ orgUserId: string;
355
+ unread: ChatUnreadSummary;
356
+ chats: ChatMemberState[];
357
+ }
358
+ interface LiveChat {
359
+ chat: Chat;
360
+ participantIds: string[];
361
+ participants: CommentAuthor[];
362
+ }
363
+ /** What the chat hub reads for each change, as current state. */
364
+ interface ChatLiveReads {
365
+ activeParticipants(orgId: string, chatId: string): Promise<string[]>;
366
+ messageViews(orgId: string, messageIds: string[]): Promise<LiveMessage[]>;
367
+ messageReactions(orgId: string, messageId: string): Promise<ReactionCounts>;
368
+ /** The chat with its active participants and their profiles. */
369
+ liveChat(orgId: string, chatId: string): Promise<LiveChat | null>;
370
+ memberStates(orgId: string, communityId: string, orgUserIds: string[]): Promise<ChatMemberStates[]>;
371
+ }
315
372
  interface ChatUnread {
316
373
  count: number;
317
374
  latest: {
@@ -337,24 +394,33 @@ interface MessageQueueEntry {
337
394
  interface MessageQueue {
338
395
  entries: MessageQueueEntry[];
339
396
  }
340
- interface ChatService {
341
- startDirect(orgId: string, actor: Actor, input: StartDirectInput): Promise<Chat>;
342
- startGroup(orgId: string, actor: Actor, input: StartGroupInput): Promise<Chat>;
343
- addParticipant(orgId: string, actor: Actor, chatId: string, orgUserId: string): Promise<Chat>;
344
- leave(orgId: string, actor: Actor, chatId: string): Promise<Chat>;
345
- accept(orgId: string, actor: Actor, chatId: string): Promise<Chat>;
346
- send(orgId: string, actor: Actor, chatId: string, input: SendMessageInput): Promise<MessageView>;
347
- editMessage(orgId: string, actor: Actor, messageId: string, body: string): Promise<MessageView>;
348
- deleteMessage(orgId: string, actor: Actor, messageId: string): Promise<MessageView>;
349
- setReaction(orgId: string, actor: Actor, messageId: string, emoji: ReactionEmoji | null): Promise<MessageReactions>;
350
- markRead(orgId: string, actor: Actor, chatId: string, messageId: string): Promise<void>;
351
- block(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;
352
- unblock(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;
353
- flagMessage(orgId: string, actor: Actor, messageId: string, reason: string): Promise<MessageFlag>;
354
- resolveMessageFlags(orgId: string, actor: Actor, messageId: string, resolution: Extract<MessageFlagResolution, 'dismissed'>, communityId?: string): Promise<void>;
355
- lock(orgId: string, actor: Actor, chatId: string, communityId?: string): Promise<Chat>;
356
- unlock(orgId: string, actor: Actor, chatId: string, communityId?: string): Promise<Chat>;
357
- listChats(orgId: string, actor: Actor, communityId: string, query: PageQuery): Promise<Page<ChatListItem>>;
397
+ interface ChatService extends ChatLiveReads {
398
+ startDirect(input: StartDirectInput, actor: Actor): Promise<Chat>;
399
+ startGroup(input: StartGroupInput, actor: Actor): Promise<Chat>;
400
+ addParticipant(chatId: string, orgUserId: string, actor: Actor): Promise<Chat>;
401
+ leave(chatId: string, actor: Actor): Promise<Chat>;
402
+ accept(chatId: string, actor: Actor): Promise<Chat>;
403
+ send(chatId: string, input: SendMessageInput, actor: Actor): Promise<MessageView>;
404
+ editMessage(messageId: string, body: string, actor: Actor): Promise<MessageView>;
405
+ deleteMessage(messageId: string, actor: Actor): Promise<MessageView>;
406
+ setReaction(messageId: string, emoji: ReactionEmoji | null, actor: Actor): Promise<MessageReactions>;
407
+ markRead(chatId: string, messageId: string, actor: Actor): Promise<void>;
408
+ block(communityId: string, orgUserId: string, actor: Actor): Promise<void>;
409
+ unblock(communityId: string, orgUserId: string, actor: Actor): Promise<void>;
410
+ flagMessage(messageId: string, reason: string, actor: Actor): Promise<MessageFlag>;
411
+ resolveMessageFlags(messageId: string, resolution: Extract<MessageFlagResolution, 'dismissed'>, communityId: string | null, actor: Actor): Promise<void>;
412
+ lock(chatId: string, communityId: string | null, actor: Actor): Promise<Chat>;
413
+ unlock(chatId: string, communityId: string | null, actor: Actor): Promise<Chat>;
414
+ listChats(communityId: string, query: ChatListQuery, actor: Actor): Promise<Page<ChatListItem>>;
415
+ /** One live stream of the member's chat events in the community. */
416
+ watch(communityId: string, actor: Actor): Promise<AsyncIterable<ChatLiveEvent>>;
417
+ chat(chatId: string, actor: Actor): Promise<ChatListItem>;
418
+ directChat(communityId: string, orgUserId: string, actor: Actor): Promise<ChatListItem>;
419
+ unreadSummary(communityId: string, actor: Actor): Promise<ChatUnreadSummary>;
420
+ typing(chatId: string, actor: Actor): Promise<void>;
421
+ /** The messages as every participant sees them: a deleted message without
422
+ * its body, attachments or mentions, and no viewer reaction. */
423
+ messageViews(orgId: string, messageIds: string[]): Promise<LiveMessage[]>;
358
424
  chatSummary(orgId: string, chatId: string): Promise<{
359
425
  chat: Chat;
360
426
  participants: ChatUser[];
@@ -363,12 +429,13 @@ interface ChatService {
363
429
  unreadInChat(orgId: string, chatId: string, orgUserId: string): Promise<ChatUnread>;
364
430
  findMessage(orgId: string, messageId: string): Promise<Message | null>;
365
431
  isBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;
366
- titlesFor(orgId: string, actor: Actor, chatIds: string[]): Promise<Record<string, string>>;
367
- messages(orgId: string, actor: Actor, chatId: string, query: Pick<FeedQuery, 'page' | 'pageSize'>): Promise<Page<MessageView>>;
432
+ titlesFor(chatIds: string[], actor: Actor): Promise<Record<string, string>>;
433
+ messages(chatId: string, query: Pick<FeedQuery, 'page' | 'pageSize'>, actor: Actor): Promise<Page<MessageView>>;
368
434
  /** Signed URL of a message attachment: for the chat's participants while the
369
- * message stands (staff also after deletion), and for staff while it is flagged. */
370
- attachmentFile(orgId: string, actor: Actor, chatId: string, messageId: string, assetId: string): Promise<string | null>;
371
- messageQueue(orgId: string, actor: Actor, communityId: string): Promise<MessageQueue>;
435
+ * message stands (moderators also after deletion), and for moderators while
436
+ * it is flagged. */
437
+ attachmentFile(chatId: string, messageId: string, assetId: string, actor: Actor): Promise<string>;
438
+ messageQueue(communityId: string, actor: Actor): Promise<MessageQueue>;
372
439
  }
373
440
  interface ChatRepository {
374
441
  lockDirectPair(orgId: string, communityId: string, orgUserId: string, otherOrgUserId: string): Promise<void>;
@@ -376,12 +443,22 @@ interface ChatRepository {
376
443
  insertChat(orgId: string, chat: Chat): Promise<Chat>;
377
444
  updateChat(orgId: string, id: string, patch: Partial<Pick<Chat, 'status' | 'lockedAt' | 'lockedBy'>>): Promise<Chat | null>;
378
445
  findChat(orgId: string, id: string): Promise<Chat | null>;
379
- listForUser(orgId: string, communityId: string, orgUserId: string, query: PageQuery): Promise<Page<ChatInboxRow>>;
446
+ listForUser(orgId: string, communityId: string, orgUserId: string, query: ChatListQuery): Promise<Page<ChatInboxRow>>;
447
+ /** The member's inbox row for one chat they are in, or null. */
448
+ inboxRow(orgId: string, chatId: string, orgUserId: string): Promise<ChatInboxRow | null>;
380
449
  participants(orgId: string, chatId: string): Promise<ChatUser[]>;
450
+ activeParticipants(orgId: string, chatId: string): Promise<string[]>;
381
451
  join(orgId: string, user: ChatUser): Promise<void>;
382
452
  markLeft(orgId: string, chatId: string, orgUserId: string, leftAt: Date): Promise<void>;
383
- setRead(orgId: string, chatId: string, orgUserId: string, messageId: string): Promise<void>;
453
+ /** Moves the read mark to the message only when it is newer (by created
454
+ * time, then id) than the current one. True when it moved. */
455
+ setRead(orgId: string, chatId: string, orgUserId: string, messageId: string): Promise<boolean>;
384
456
  unreadFor(orgId: string, chatId: string, orgUserId: string): Promise<ChatUnread>;
457
+ /** Unread messages across the member's chats in the community that are
458
+ * neither muted at `at` nor pending requests to them, plus those requests. */
459
+ unreadSummary(orgId: string, communityId: string, orgUserId: string, at: Date): Promise<ChatUnreadSummary>;
460
+ memberStates(orgId: string, communityId: string, orgUserIds: string[], at: Date): Promise<ChatMemberStates[]>;
461
+ findByClientId(orgId: string, chatId: string, orgUserId: string, clientId: string): Promise<Message | null>;
385
462
  insertMessage(orgId: string, message: Message): Promise<Message>;
386
463
  updateMessage(orgId: string, id: string, patch: Partial<Pick<Message, 'body' | 'editedAt' | 'deletedAt'>>): Promise<Message | null>;
387
464
  findMessage(orgId: string, id: string): Promise<Message | null>;
@@ -389,7 +466,7 @@ interface ChatRepository {
389
466
  attach(orgId: string, communityId: string, messageId: string, assetIds: string[]): Promise<void>;
390
467
  attachmentsOf(orgId: string, messageIds: string[]): Promise<Record<string, string[]>>;
391
468
  blockedWith(orgId: string, communityId: string, orgUserId: string, otherIds: string[]): Promise<string[]>;
392
- setReaction(orgId: string, communityId: string, messageId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<boolean>;
469
+ setReaction(orgId: string, communityId: string, messageId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<ReactionChange>;
393
470
  reactionsOf(orgId: string, messageIds: string[], viewerId: string): Promise<Record<string, MessageReactions>>;
394
471
  block(orgId: string, communityId: string, blockerOrgUserId: string, blockedOrgUserId: string): Promise<void>;
395
472
  unblock(orgId: string, communityId: string, blockerOrgUserId: string, blockedOrgUserId: string): Promise<void>;
@@ -428,10 +505,15 @@ type SubscriptionInput = {
428
505
  type MuteInput = Exclude<SubscriptionInput, {
429
506
  state: 'following';
430
507
  }>;
508
+ interface SubscriptionWriteScope {
509
+ subscriptions: SubscriptionsRepository;
510
+ outbox: OutboxAppender;
511
+ }
512
+ type SubscriptionUnitOfWork = UnitOfWork<SubscriptionWriteScope>;
431
513
  interface SubscriptionService {
432
- setPost(orgId: string, actor: Actor, postId: string, input: SubscriptionInput): Promise<void>;
433
- setSpace(orgId: string, actor: Actor, spaceId: string, input: MuteInput): Promise<void>;
434
- setChat(orgId: string, actor: Actor, chatId: string, input: MuteInput): Promise<void>;
514
+ setPost(postId: string, input: SubscriptionInput, actor: Actor): Promise<void>;
515
+ setSpace(spaceId: string, input: MuteInput, actor: Actor): Promise<void>;
516
+ setChat(chatId: string, input: MuteInput, actor: Actor): Promise<void>;
435
517
  viewerSubscriptions(orgId: string, orgUserId: string, itemType: SubscriptionItemType, itemIds: string[], at: Date): Promise<Record<string, SubscriptionView>>;
436
518
  followerIds(orgId: string, postId: string): Promise<string[]>;
437
519
  mutedAmong(orgId: string, item: SubjectRef, orgUserIds: string[], at: Date): Promise<string[]>;
@@ -452,5 +534,5 @@ interface MentionsRepository {
452
534
  removeItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<void>;
453
535
  }
454
536
  //#endregion
455
- export { ActivityComments, ActivityGetter, type Actor, AuthorProfileReader, AuthorRecord, AuthorRoleReader, BookmarkQuery, ChatInboxRow, ChatListItem, ChatRepository, ChatService, ChatSummary, ChatUnitOfWork, ChatUnread, ChatWriteScope, CommentReactions, CommentWithContext, ContentAccessReader, CreatePostInput, DigestRunRepository, DigestScheduleReader, DigestService, DigestUnitOfWork, DigestWriteScope, DiscussionRepository, DiscussionService, DiscussionUnitOfWork, DiscussionWriteScope, FeedQuery, ItemTitle, ItemTitles, MODERATION_QUEUE_LIMIT, MentionsRepository, MessageQueue, MessageQueueEntry, MessageReactions, MuteInput, PageQuery, PostCommentInput, PostCommentReader, PostGuard, PostMode, PostPatch, PostReactions, PostRepository, PostService, PostThreadsQuery, PostUnitOfWork, PostWriteScope, PublishedPost, SendMessageInput, SpaceModerationQueue, StartDirectInput, StartGroupInput, SubscriptionInput, SubscriptionService, SubscriptionsRepository, UpdatePostInput };
537
+ export { ActivityComments, ActivityGetter, type Actor, AuthorPermissionReader, AuthorProfileReader, AuthorRecord, BookmarkQuery, ChatInboxRow, ChatListItem, ChatListQuery, ChatLiveReads, ChatMemberState, ChatMemberStates, ChatRepository, ChatService, ChatSummary, ChatUnitOfWork, ChatUnread, ChatWriteScope, CommentReactions, CommentWithContext, CommentWithin, CommunityAccess, ContentAccessReader, CreatePostInput, DigestRunRepository, DigestScheduleReader, DigestService, DigestUnitOfWork, DigestWriteScope, DiscussionRepository, DiscussionService, DiscussionUnitOfWork, DiscussionWriteScope, FeedQuery, ItemTitle, ItemTitles, LiveChat, MODERATION_QUEUE_LIMIT, MentionsRepository, MessageQueue, MessageQueueEntry, MessageReactions, MuteInput, OpenableSpace, PageQuery, PostCommentInput, PostCommentReader, PostGuard, PostMode, PostPatch, PostReactions, PostRepository, PostService, PostThreadsQuery, PostUnitOfWork, PostWriteScope, PublishedPost, ReactionChange, SendMessageInput, SpaceModerationQueue, SpaceReader, StartDirectInput, StartGroupInput, SubscriptionInput, SubscriptionService, SubscriptionUnitOfWork, SubscriptionWriteScope, SubscriptionsRepository, UpdatePostInput };
456
538
  //# sourceMappingURL=ports.d.ts.map