@headless-lms/core 0.3.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 (307) hide show
  1. package/dist/assets/events.d.ts +24 -18
  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 +5 -4
  6. package/dist/assets/index.js +4 -3
  7. package/dist/assets/jobs.d.ts +15 -0
  8. package/dist/assets/jobs.js +30 -0
  9. package/dist/assets/jobs.js.map +1 -0
  10. package/dist/assets/ports.d.ts +37 -17
  11. package/dist/assets/service.d.ts +30 -15
  12. package/dist/assets/service.js +229 -59
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.js +2 -63
  15. package/dist/automations/actions.js.map +1 -1
  16. package/dist/automations/catalog.d.ts +1 -2
  17. package/dist/automations/catalog.js +137 -65
  18. package/dist/automations/catalog.js.map +1 -1
  19. package/dist/automations/community-derivations.js +20 -20
  20. package/dist/automations/community-derivations.js.map +1 -1
  21. package/dist/automations/community-mail.d.ts +36 -20
  22. package/dist/automations/community-mail.js +32 -24
  23. package/dist/automations/community-mail.js.map +1 -1
  24. package/dist/automations/event-permissions.d.ts +12 -0
  25. package/dist/automations/event-permissions.js +38 -0
  26. package/dist/automations/event-permissions.js.map +1 -0
  27. package/dist/automations/events.d.ts +0 -10
  28. package/dist/automations/index.d.ts +12 -4
  29. package/dist/automations/index.js +9 -1
  30. package/dist/automations/notifications/applicable.d.ts +8 -0
  31. package/dist/automations/notifications/applicable.js +12 -0
  32. package/dist/automations/notifications/applicable.js.map +1 -0
  33. package/dist/automations/notifications/debounce.d.ts +25 -0
  34. package/dist/automations/notifications/debounce.js +93 -0
  35. package/dist/automations/notifications/debounce.js.map +1 -0
  36. package/dist/automations/notifications/defaults.d.ts +8 -0
  37. package/dist/automations/notifications/defaults.js +346 -0
  38. package/dist/automations/notifications/defaults.js.map +1 -0
  39. package/dist/automations/notifications/delivery.d.ts +28 -0
  40. package/dist/automations/notifications/delivery.js +88 -0
  41. package/dist/automations/notifications/delivery.js.map +1 -0
  42. package/dist/automations/notifications/processor.d.ts +64 -0
  43. package/dist/automations/notifications/processor.js +270 -0
  44. package/dist/automations/notifications/processor.js.map +1 -0
  45. package/dist/automations/notifications/rules/content.d.ts +6 -0
  46. package/dist/automations/notifications/rules/content.js +96 -0
  47. package/dist/automations/notifications/rules/content.js.map +1 -0
  48. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  49. package/dist/automations/notifications/rules/discussion.js +678 -0
  50. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  51. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  52. package/dist/automations/notifications/rules/entitlements.js +276 -0
  53. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  54. package/dist/automations/notifications/rules/events.d.ts +9 -0
  55. package/dist/automations/notifications/rules/events.js +27 -0
  56. package/dist/automations/notifications/rules/events.js.map +1 -0
  57. package/dist/automations/notifications/rules/index.d.ts +6 -0
  58. package/dist/automations/notifications/rules/index.js +15 -0
  59. package/dist/automations/notifications/rules/index.js.map +1 -0
  60. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  61. package/dist/automations/notifications/rules/organizations.js +35 -0
  62. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  63. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  64. package/dist/automations/notifications/rules/recipients.js +98 -0
  65. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  66. package/dist/automations/notifications/service.d.ts +29 -0
  67. package/dist/automations/notifications/service.js +45 -0
  68. package/dist/automations/notifications/service.js.map +1 -0
  69. package/dist/automations/notifications/types.d.ts +7 -0
  70. package/dist/automations/notifications/types.js +560 -0
  71. package/dist/automations/notifications/types.js.map +1 -0
  72. package/dist/automations/ports.d.ts +134 -21
  73. package/dist/automations/service.d.ts +30 -30
  74. package/dist/automations/service.js +111 -144
  75. package/dist/automations/service.js.map +1 -1
  76. package/dist/content/event-sight.d.ts +11 -0
  77. package/dist/content/event-sight.js +11 -0
  78. package/dist/content/event-sight.js.map +1 -0
  79. package/dist/content/events.d.ts +437 -46
  80. package/dist/content/events.js +45 -1
  81. package/dist/content/events.js.map +1 -1
  82. package/dist/content/index.d.ts +8 -4
  83. package/dist/content/index.js +5 -1
  84. package/dist/content/jobs.d.ts +16 -0
  85. package/dist/content/jobs.js +37 -0
  86. package/dist/content/jobs.js.map +1 -0
  87. package/dist/content/model.d.ts +2 -2
  88. package/dist/content/notification-categories.d.ts +37 -0
  89. package/dist/content/notification-categories.js +73 -0
  90. package/dist/content/notification-categories.js.map +1 -0
  91. package/dist/content/ports.d.ts +132 -68
  92. package/dist/content/service.d.ts +100 -68
  93. package/dist/content/service.js +703 -146
  94. package/dist/content/service.js.map +1 -1
  95. package/dist/content/titles.d.ts +25 -0
  96. package/dist/content/titles.js +82 -0
  97. package/dist/content/titles.js.map +1 -0
  98. package/dist/content/types.d.ts +2 -2
  99. package/dist/discussion/authors.d.ts +4 -2
  100. package/dist/discussion/authors.js +9 -5
  101. package/dist/discussion/authors.js.map +1 -1
  102. package/dist/discussion/chats.d.ts +63 -39
  103. package/dist/discussion/chats.js +455 -190
  104. package/dist/discussion/chats.js.map +1 -1
  105. package/dist/discussion/digests.d.ts +24 -0
  106. package/dist/discussion/digests.js +104 -0
  107. package/dist/discussion/digests.js.map +1 -0
  108. package/dist/discussion/events.d.ts +227 -23
  109. package/dist/discussion/events.js +114 -19
  110. package/dist/discussion/events.js.map +1 -1
  111. package/dist/discussion/index.d.ts +12 -4
  112. package/dist/discussion/index.js +9 -2
  113. package/dist/discussion/jobs.d.ts +16 -0
  114. package/dist/discussion/jobs.js +40 -0
  115. package/dist/discussion/jobs.js.map +1 -0
  116. package/dist/discussion/live.d.ts +106 -0
  117. package/dist/discussion/live.js +642 -0
  118. package/dist/discussion/live.js.map +1 -0
  119. package/dist/discussion/mentions.d.ts +11 -0
  120. package/dist/discussion/mentions.js +35 -0
  121. package/dist/discussion/mentions.js.map +1 -0
  122. package/dist/discussion/ports.d.ts +279 -114
  123. package/dist/discussion/ports.js.map +1 -1
  124. package/dist/discussion/posts.d.ts +60 -44
  125. package/dist/discussion/posts.js +453 -287
  126. package/dist/discussion/posts.js.map +1 -1
  127. package/dist/discussion/rules.d.ts +18 -0
  128. package/dist/discussion/rules.js +22 -0
  129. package/dist/discussion/rules.js.map +1 -0
  130. package/dist/discussion/service.d.ts +50 -35
  131. package/dist/discussion/service.js +333 -205
  132. package/dist/discussion/service.js.map +1 -1
  133. package/dist/discussion/subscriptions.d.ts +37 -0
  134. package/dist/discussion/subscriptions.js +115 -0
  135. package/dist/discussion/subscriptions.js.map +1 -0
  136. package/dist/discussion/titles.d.ts +28 -0
  137. package/dist/discussion/titles.js +98 -0
  138. package/dist/discussion/titles.js.map +1 -0
  139. package/dist/entitlements/events.d.ts +20 -20
  140. package/dist/entitlements/index.d.ts +5 -4
  141. package/dist/entitlements/index.js +3 -2
  142. package/dist/entitlements/jobs.d.ts +12 -0
  143. package/dist/entitlements/jobs.js +18 -0
  144. package/dist/entitlements/jobs.js.map +1 -0
  145. package/dist/entitlements/model.d.ts +2 -2
  146. package/dist/entitlements/ports.d.ts +68 -38
  147. package/dist/entitlements/rules.d.ts +3 -6
  148. package/dist/entitlements/rules.js +5 -10
  149. package/dist/entitlements/rules.js.map +1 -1
  150. package/dist/entitlements/service.d.ts +43 -28
  151. package/dist/entitlements/service.js +387 -160
  152. package/dist/entitlements/service.js.map +1 -1
  153. package/dist/identity/devices.d.ts +30 -0
  154. package/dist/identity/devices.js +109 -0
  155. package/dist/identity/devices.js.map +1 -0
  156. package/dist/identity/index.d.ts +6 -5
  157. package/dist/identity/index.js +4 -3
  158. package/dist/identity/model.d.ts +2 -2
  159. package/dist/identity/notification-settings.d.ts +28 -0
  160. package/dist/identity/notification-settings.js +114 -0
  161. package/dist/identity/notification-settings.js.map +1 -0
  162. package/dist/identity/ports.d.ts +80 -56
  163. package/dist/identity/service.d.ts +8 -7
  164. package/dist/identity/service.js +17 -13
  165. package/dist/identity/service.js.map +1 -1
  166. package/dist/integrations/ports.d.ts +9 -8
  167. package/dist/integrations/service.d.ts +9 -8
  168. package/dist/integrations/service.js +33 -16
  169. package/dist/integrations/service.js.map +1 -1
  170. package/dist/organizations/default-roles.d.ts +22 -0
  171. package/dist/organizations/default-roles.js +89 -0
  172. package/dist/organizations/default-roles.js.map +1 -0
  173. package/dist/organizations/effective-permissions.d.ts +45 -0
  174. package/dist/organizations/effective-permissions.js +113 -0
  175. package/dist/organizations/effective-permissions.js.map +1 -0
  176. package/dist/organizations/events.d.ts +320 -181
  177. package/dist/organizations/events.js +90 -18
  178. package/dist/organizations/events.js.map +1 -1
  179. package/dist/organizations/index.d.ts +15 -10
  180. package/dist/organizations/index.js +8 -3
  181. package/dist/organizations/members.d.ts +66 -8
  182. package/dist/organizations/members.js.map +1 -1
  183. package/dist/organizations/notification-catalog.d.ts +31 -0
  184. package/dist/organizations/notification-catalog.js +32 -0
  185. package/dist/organizations/notification-catalog.js.map +1 -0
  186. package/dist/organizations/notification-categories.d.ts +34 -0
  187. package/dist/organizations/notification-categories.js +66 -0
  188. package/dist/organizations/notification-categories.js.map +1 -0
  189. package/dist/organizations/org-roles.d.ts +33 -0
  190. package/dist/organizations/org-roles.js +49 -0
  191. package/dist/organizations/org-roles.js.map +1 -0
  192. package/dist/organizations/permissions.d.ts +311 -0
  193. package/dist/organizations/permissions.js +327 -0
  194. package/dist/organizations/permissions.js.map +1 -0
  195. package/dist/organizations/ports.d.ts +187 -65
  196. package/dist/organizations/service.d.ts +104 -35
  197. package/dist/organizations/service.js +1247 -323
  198. package/dist/organizations/service.js.map +1 -1
  199. package/dist/organizations/testing.d.ts +18 -0
  200. package/dist/organizations/testing.js +37 -0
  201. package/dist/organizations/testing.js.map +1 -0
  202. package/dist/organizations/types.d.ts +5 -16
  203. package/dist/progress/index.d.ts +2 -2
  204. package/dist/progress/ports.d.ts +11 -3
  205. package/dist/progress/service.d.ts +6 -5
  206. package/dist/progress/service.js +23 -8
  207. package/dist/progress/service.js.map +1 -1
  208. package/dist/reporting/admin/index.d.ts +6 -4
  209. package/dist/reporting/admin/model.d.ts +36 -3
  210. package/dist/reporting/admin/ports.d.ts +39 -14
  211. package/dist/reporting/admin/service.d.ts +32 -11
  212. package/dist/reporting/admin/service.js +102 -18
  213. package/dist/reporting/admin/service.js.map +1 -1
  214. package/dist/reporting/events/index.d.ts +27 -0
  215. package/dist/reporting/events/index.js +20 -0
  216. package/dist/reporting/events/index.js.map +1 -0
  217. package/dist/reporting/member/calendar.d.ts +11 -0
  218. package/dist/reporting/member/calendar.js +75 -0
  219. package/dist/reporting/member/calendar.js.map +1 -0
  220. package/dist/reporting/member/index.d.ts +3 -2
  221. package/dist/reporting/member/model.d.ts +80 -26
  222. package/dist/reporting/member/ports.d.ts +50 -28
  223. package/dist/reporting/member/service.d.ts +81 -35
  224. package/dist/reporting/member/service.js +438 -145
  225. package/dist/reporting/member/service.js.map +1 -1
  226. package/dist/shared/actor.d.ts +22 -3
  227. package/dist/shared/errors.d.ts +15 -2
  228. package/dist/shared/errors.js +21 -2
  229. package/dist/shared/errors.js.map +1 -1
  230. package/dist/shared/id.d.ts +7 -0
  231. package/dist/shared/id.js +8 -1
  232. package/dist/shared/id.js.map +1 -1
  233. package/dist/shared/logger.d.ts +2 -1
  234. package/dist/shared/logger.js +10 -1
  235. package/dist/shared/logger.js.map +1 -1
  236. package/dist/shared/mailer.d.ts +11 -1
  237. package/dist/shared/mailer.js +32 -5
  238. package/dist/shared/mailer.js.map +1 -1
  239. package/dist/shared/notification-categories.d.ts +16 -0
  240. package/dist/shared/notification-categories.js +94 -0
  241. package/dist/shared/notification-categories.js.map +1 -0
  242. package/dist/shared/notification-settings.d.ts +14 -0
  243. package/dist/shared/notification-settings.js +61 -0
  244. package/dist/shared/notification-settings.js.map +1 -0
  245. package/dist/shared/ports.d.ts +68 -2
  246. package/dist/shared/settings.d.ts +2 -1
  247. package/dist/shared/settings.js +1 -0
  248. package/dist/shared/settings.js.map +1 -1
  249. package/dist/types/automations.d.ts +8 -2
  250. package/dist/types/content.d.ts +2 -2
  251. package/dist/types/discussion.d.ts +4 -5
  252. package/dist/types/email-templates.d.ts +119 -6
  253. package/dist/types/entitlements.d.ts +2 -2
  254. package/dist/types/identity.d.ts +14 -17
  255. package/dist/types/index.d.ts +13 -12
  256. package/dist/types/index.js +2 -1
  257. package/dist/types/notifications.d.ts +157 -0
  258. package/dist/types/notifications.js +1 -0
  259. package/dist/types/organizations.d.ts +3 -2
  260. package/dist/types/ports.d.ts +8 -1
  261. package/dist/types/ports.js +7 -1
  262. package/dist/types/ports.js.map +1 -0
  263. package/dist/types/schemas/assets.d.ts +5 -5
  264. package/dist/types/schemas/assets.js +4 -3
  265. package/dist/types/schemas/assets.js.map +1 -1
  266. package/dist/types/schemas/automations.d.ts +1 -10
  267. package/dist/types/schemas/automations.js +1 -3
  268. package/dist/types/schemas/automations.js.map +1 -1
  269. package/dist/types/schemas/content.d.ts +596 -11
  270. package/dist/types/schemas/content.js +225 -9
  271. package/dist/types/schemas/content.js.map +1 -1
  272. package/dist/types/schemas/discussion.d.ts +362 -66
  273. package/dist/types/schemas/discussion.js +123 -15
  274. package/dist/types/schemas/discussion.js.map +1 -1
  275. package/dist/types/schemas/entitlements.d.ts +14 -1
  276. package/dist/types/schemas/entitlements.js +11 -4
  277. package/dist/types/schemas/entitlements.js.map +1 -1
  278. package/dist/types/schemas/identity.d.ts +1 -85
  279. package/dist/types/schemas/identity.js +1 -42
  280. package/dist/types/schemas/identity.js.map +1 -1
  281. package/dist/types/schemas/index.d.ts +8 -7
  282. package/dist/types/schemas/index.js +8 -7
  283. package/dist/types/schemas/notifications.d.ts +96 -0
  284. package/dist/types/schemas/notifications.js +57 -0
  285. package/dist/types/schemas/notifications.js.map +1 -0
  286. package/dist/types/schemas/organizations.d.ts +96 -60
  287. package/dist/types/schemas/organizations.js +78 -27
  288. package/dist/types/schemas/organizations.js.map +1 -1
  289. package/dist/types/schemas/progress.d.ts +0 -1
  290. package/dist/types/schemas/progress.js +0 -1
  291. package/dist/types/schemas/progress.js.map +1 -1
  292. package/package.json +1 -1
  293. package/dist/automations/community-notifications.d.ts +0 -16
  294. package/dist/automations/community-notifications.js +0 -84
  295. package/dist/automations/community-notifications.js.map +0 -1
  296. package/dist/automations/seeds.d.ts +0 -17
  297. package/dist/automations/seeds.js +0 -235
  298. package/dist/automations/seeds.js.map +0 -1
  299. package/dist/identity/notifications.d.ts +0 -27
  300. package/dist/identity/notifications.js +0 -101
  301. package/dist/identity/notifications.js.map +0 -1
  302. package/dist/organizations/access-scopes.d.ts +0 -19
  303. package/dist/organizations/access-scopes.js +0 -89
  304. package/dist/organizations/access-scopes.js.map +0 -1
  305. package/dist/organizations/roles.d.ts +0 -19
  306. package/dist/organizations/roles.js +0 -82
  307. package/dist/organizations/roles.js.map +0 -1
@@ -0,0 +1,642 @@
1
+ import { describeError, noopLogger } from "../shared/logger.js";
2
+ import { discussionEvents } from "./events.js";
3
+ import { z } from "zod";
4
+ //#region src/discussion/live.ts
5
+ const CHAT_CHANNEL = "discussion.chats";
6
+ const TYPING = "typing";
7
+ const HEARTBEAT_MS = 25e3;
8
+ const PUBLISH_TIMEOUT_MS = 2e3;
9
+ const FLUSH_MS = 1e3;
10
+ const CLOSE_TIMEOUT_MS = 2e3;
11
+ const MAX_CHATS = 16;
12
+ const MAX_PENDING = 5e3;
13
+ const STREAM_QUEUE = 256;
14
+ const SENT = discussionEvents.messageSent.type;
15
+ const UPDATED = discussionEvents.messageUpdated.type;
16
+ const DELETED = discussionEvents.messageDeleted.type;
17
+ const REACTIONS = discussionEvents.messageReactionsChanged.type;
18
+ const CREATED = discussionEvents.chatCreated.type;
19
+ const CHAT_UPDATED = discussionEvents.chatUpdated.type;
20
+ const ADDED = discussionEvents.chatParticipantAdded.type;
21
+ const READ = discussionEvents.chatRead.type;
22
+ const MUTED = discussionEvents.chatMuted.type;
23
+ const chatChangeSchema = z.object({
24
+ event: z.string(),
25
+ orgId: z.string(),
26
+ communityId: z.string(),
27
+ chatId: z.string(),
28
+ messageId: z.string().optional(),
29
+ orgUserId: z.string().optional(),
30
+ members: z.array(z.string()).optional(),
31
+ reaction: z.string().nullable().optional(),
32
+ until: z.string().optional()
33
+ });
34
+ /** Publishes a change, logging and dropping it when the publish fails or has
35
+ * not finished within 2 s. Never throws. */
36
+ async function publishChange(pubsub, change, logger, timeoutMs = PUBLISH_TIMEOUT_MS) {
37
+ let timer;
38
+ try {
39
+ await Promise.race([pubsub.publish(CHAT_CHANNEL, change), new Promise((_, reject) => {
40
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("publish timed out")), timeoutMs);
41
+ })]);
42
+ } catch (error) {
43
+ logger.warn("live publish failed", {
44
+ orgId: change.orgId,
45
+ chatId: change.chatId,
46
+ event: change.event,
47
+ error: describeError(error)
48
+ });
49
+ } finally {
50
+ clearTimeout(timer);
51
+ }
52
+ }
53
+ function actorOf(event) {
54
+ const actor = event.metadata?.actorId;
55
+ return typeof actor === "string" ? [actor] : [];
56
+ }
57
+ function union(...lists) {
58
+ return [...new Set(lists.flat())].sort();
59
+ }
60
+ /** Publishes every chat change committed through the outbox to every instance. */
61
+ var ChatLiveForwarder = class {
62
+ pubsub;
63
+ logger;
64
+ timeoutMs;
65
+ constructor(params) {
66
+ this.pubsub = params.pubsub;
67
+ this.logger = params.logger ?? noopLogger;
68
+ this.timeoutMs = params.timeoutMs ?? PUBLISH_TIMEOUT_MS;
69
+ }
70
+ attach(bus) {
71
+ const forward = (change) => publishChange(this.pubsub, change, this.logger, this.timeoutMs);
72
+ const chat = (event) => forward({
73
+ event: event.type,
74
+ orgId: event.orgId,
75
+ communityId: event.data.communityId,
76
+ chatId: event.data.id,
77
+ members: union(event.data.participantIds, actorOf(event))
78
+ });
79
+ const message = (event) => forward({
80
+ event: event.type,
81
+ orgId: event.orgId,
82
+ communityId: event.data.communityId,
83
+ chatId: event.data.chatId,
84
+ messageId: event.data.id
85
+ });
86
+ const member = (event) => forward({
87
+ event: event.type,
88
+ orgId: event.orgId,
89
+ communityId: event.data.communityId,
90
+ chatId: event.data.chatId,
91
+ orgUserId: event.data.orgUserId
92
+ });
93
+ bus.subscribe(discussionEvents.chatCreated, chat);
94
+ bus.subscribe(discussionEvents.chatUpdated, chat);
95
+ bus.subscribe(discussionEvents.chatParticipantAdded, (event) => forward({
96
+ event: event.type,
97
+ orgId: event.orgId,
98
+ communityId: event.data.communityId,
99
+ chatId: event.data.chatId,
100
+ orgUserId: event.data.orgUserId,
101
+ members: union([event.data.orgUserId], actorOf(event))
102
+ }));
103
+ bus.subscribe(discussionEvents.messageSent, message);
104
+ bus.subscribe(discussionEvents.messageUpdated, message);
105
+ bus.subscribe(discussionEvents.messageDeleted, message);
106
+ bus.subscribe(discussionEvents.messageReactionsChanged, (event) => forward({
107
+ event: event.type,
108
+ orgId: event.orgId,
109
+ communityId: event.data.communityId,
110
+ chatId: event.data.chatId,
111
+ messageId: event.data.messageId,
112
+ orgUserId: event.data.orgUserId,
113
+ reaction: event.data.reaction
114
+ }));
115
+ bus.subscribe(discussionEvents.chatRead, member);
116
+ bus.subscribe(discussionEvents.chatMuted, member);
117
+ }
118
+ };
119
+ var LiveStream = class {
120
+ orgId;
121
+ communityId;
122
+ orgUserId;
123
+ onEnd;
124
+ onOverflow;
125
+ done;
126
+ queue = [];
127
+ waiter = null;
128
+ ending = false;
129
+ finished = false;
130
+ resolveDone;
131
+ constructor(orgId, communityId, orgUserId, onEnd, onOverflow) {
132
+ this.orgId = orgId;
133
+ this.communityId = communityId;
134
+ this.orgUserId = orgUserId;
135
+ this.onEnd = onEnd;
136
+ this.onOverflow = onOverflow;
137
+ this.done = new Promise((resolve) => {
138
+ this.resolveDone = resolve;
139
+ });
140
+ }
141
+ get ended() {
142
+ return this.ending;
143
+ }
144
+ push(event) {
145
+ if (this.ending) return;
146
+ const waiter = this.waiter;
147
+ if (waiter) {
148
+ this.waiter = null;
149
+ waiter({
150
+ value: event,
151
+ done: false
152
+ });
153
+ return;
154
+ }
155
+ if (this.queue.length >= STREAM_QUEUE) {
156
+ this.queue = [{ type: "resync" }];
157
+ this.onOverflow(this);
158
+ return;
159
+ }
160
+ this.queue.push(event);
161
+ }
162
+ end(last) {
163
+ if (this.ending) return;
164
+ if (last) this.push(last);
165
+ this.ending = true;
166
+ this.onEnd(this);
167
+ const waiter = this.waiter;
168
+ if (waiter && this.queue.length === 0) {
169
+ this.waiter = null;
170
+ this.finish();
171
+ waiter({
172
+ value: void 0,
173
+ done: true
174
+ });
175
+ }
176
+ }
177
+ next() {
178
+ const event = this.queue.shift();
179
+ if (event) return Promise.resolve({
180
+ value: event,
181
+ done: false
182
+ });
183
+ if (this.ending) {
184
+ this.finish();
185
+ return Promise.resolve({
186
+ value: void 0,
187
+ done: true
188
+ });
189
+ }
190
+ return new Promise((resolve) => {
191
+ this.waiter = resolve;
192
+ });
193
+ }
194
+ return() {
195
+ this.queue = [];
196
+ this.end();
197
+ const waiter = this.waiter;
198
+ if (waiter) {
199
+ this.waiter = null;
200
+ waiter({
201
+ value: void 0,
202
+ done: true
203
+ });
204
+ }
205
+ this.finish();
206
+ return Promise.resolve({
207
+ value: void 0,
208
+ done: true
209
+ });
210
+ }
211
+ [Symbol.asyncIterator]() {
212
+ return this;
213
+ }
214
+ finish() {
215
+ if (this.finished) return;
216
+ this.finished = true;
217
+ this.resolveDone();
218
+ }
219
+ };
220
+ function coalesceKey(change) {
221
+ switch (change.event) {
222
+ case SENT:
223
+ case UPDATED:
224
+ case DELETED: return `message:${change.messageId}`;
225
+ case REACTIONS: return `reaction:${change.messageId}:${change.orgUserId}`;
226
+ case CREATED:
227
+ case CHAT_UPDATED:
228
+ case ADDED: return "chat";
229
+ case READ:
230
+ case MUTED: return `member:${change.orgUserId}`;
231
+ default: return null;
232
+ }
233
+ }
234
+ function merge(existing, next) {
235
+ switch (existing.event) {
236
+ case SENT:
237
+ case UPDATED:
238
+ case DELETED: {
239
+ const events = [existing.event, next.event];
240
+ const event = events.includes(SENT) ? SENT : events.includes(DELETED) ? DELETED : UPDATED;
241
+ return {
242
+ ...next,
243
+ event
244
+ };
245
+ }
246
+ case CREATED:
247
+ case CHAT_UPDATED:
248
+ case ADDED: return {
249
+ ...existing,
250
+ members: union(existing.members ?? [], next.members ?? [])
251
+ };
252
+ case REACTIONS: return next;
253
+ default: return existing;
254
+ }
255
+ }
256
+ const key = (orgId, id) => `${orgId}\u0000${id}`;
257
+ /** This process's open chat streams, and the live delivery of every chat
258
+ * change to them. */
259
+ var ChatHub = class {
260
+ pubsub;
261
+ reads;
262
+ logger;
263
+ flushMs;
264
+ closeTimeoutMs;
265
+ streams = /* @__PURE__ */ new Set();
266
+ byMember = /* @__PURE__ */ new Map();
267
+ byCommunity = /* @__PURE__ */ new Map();
268
+ queues = /* @__PURE__ */ new Map();
269
+ ready = [];
270
+ running = 0;
271
+ pending = 0;
272
+ dirty = /* @__PURE__ */ new Map();
273
+ flushing = false;
274
+ flushAgain = false;
275
+ closing = false;
276
+ unsubscribe = null;
277
+ flushTimer = null;
278
+ constructor(params) {
279
+ this.pubsub = params.pubsub;
280
+ this.reads = params.reads;
281
+ this.logger = params.logger ?? noopLogger;
282
+ this.flushMs = params.flushMs ?? FLUSH_MS;
283
+ this.closeTimeoutMs = params.closeTimeoutMs ?? CLOSE_TIMEOUT_MS;
284
+ }
285
+ async start() {
286
+ if (this.unsubscribe || this.closing) return;
287
+ this.unsubscribe = this.pubsub.subscribe(CHAT_CHANNEL, (message) => this.receive(message), () => this.reset());
288
+ this.flushTimer = setInterval(() => {
289
+ if (!this.flushing) this.flush();
290
+ }, this.flushMs);
291
+ this.flushTimer.unref();
292
+ this.logger.info("chat hub started");
293
+ }
294
+ /** Ends every stream with `closed { reason: 'shutdown' }` and waits, up to
295
+ * 2 s in all, for each to be read to its end. */
296
+ async close() {
297
+ if (this.closing) return;
298
+ this.closing = true;
299
+ this.unsubscribe?.();
300
+ this.unsubscribe = null;
301
+ if (this.flushTimer) clearInterval(this.flushTimer);
302
+ this.queues.clear();
303
+ this.ready = [];
304
+ this.pending = 0;
305
+ const streams = [...this.streams];
306
+ for (const stream of streams) stream.end({
307
+ type: "closed",
308
+ reason: "shutdown"
309
+ });
310
+ let timer;
311
+ await Promise.race([Promise.all(streams.map((stream) => stream.done)), new Promise((resolve) => {
312
+ timer = setTimeout(resolve, this.closeTimeoutMs);
313
+ })]);
314
+ clearTimeout(timer);
315
+ this.logger.info("chat hub closed", { streams: streams.length });
316
+ }
317
+ /** Registers one stream. It starts with `ready`, then the member's unread. */
318
+ open(registration) {
319
+ const { actor, communityId } = registration;
320
+ const stream = new LiveStream(actor.orgId, communityId, actor.id, (ended) => this.unregister(ended), (full) => this.markDirty(full.orgId, full.communityId, full.orgUserId));
321
+ if (this.closing) {
322
+ stream.end({
323
+ type: "closed",
324
+ reason: "shutdown"
325
+ });
326
+ return stream;
327
+ }
328
+ this.streams.add(stream);
329
+ this.index(this.byMember, key(stream.orgId, stream.orgUserId), stream);
330
+ this.index(this.byCommunity, key(stream.orgId, stream.communityId), stream);
331
+ stream.push({
332
+ type: "ready",
333
+ heartbeatMs: HEARTBEAT_MS
334
+ });
335
+ this.markDirty(stream.orgId, stream.communityId, stream.orgUserId);
336
+ this.flush();
337
+ this.logger.debug("chat stream opened", {
338
+ orgId: stream.orgId,
339
+ communityId,
340
+ streams: this.streams.size
341
+ });
342
+ return stream;
343
+ }
344
+ index(map, at, stream) {
345
+ const set = map.get(at) ?? /* @__PURE__ */ new Set();
346
+ set.add(stream);
347
+ map.set(at, set);
348
+ }
349
+ unindex(map, at, stream) {
350
+ const set = map.get(at);
351
+ set?.delete(stream);
352
+ if (set?.size === 0) map.delete(at);
353
+ }
354
+ unregister(stream) {
355
+ if (!this.streams.delete(stream)) return;
356
+ this.unindex(this.byMember, key(stream.orgId, stream.orgUserId), stream);
357
+ this.unindex(this.byCommunity, key(stream.orgId, stream.communityId), stream);
358
+ this.logger.debug("chat stream closed", {
359
+ orgId: stream.orgId,
360
+ communityId: stream.communityId,
361
+ streams: this.streams.size
362
+ });
363
+ }
364
+ streamsOf(orgId, communityId, orgUserId) {
365
+ return [...this.byMember.get(key(orgId, orgUserId)) ?? []].filter((stream) => stream.communityId === communityId);
366
+ }
367
+ communityStreams(orgId, communityId) {
368
+ return [...this.byCommunity.get(key(orgId, communityId)) ?? []];
369
+ }
370
+ receive(message) {
371
+ const parsed = chatChangeSchema.safeParse(message);
372
+ if (!parsed.success) {
373
+ this.logger.warn("chat change unreadable");
374
+ return;
375
+ }
376
+ const change = parsed.data;
377
+ if (this.closing || !this.byCommunity.has(key(change.orgId, change.communityId))) return;
378
+ const at = key(change.orgId, change.chatId);
379
+ const queue = this.queues.get(at) ?? {
380
+ pending: [],
381
+ running: false,
382
+ queued: false
383
+ };
384
+ this.queues.set(at, queue);
385
+ const coalesce = coalesceKey(change);
386
+ const index = coalesce === null ? -1 : queue.pending.findIndex((other) => coalesceKey(other) === coalesce);
387
+ if (index >= 0) queue.pending[index] = merge(queue.pending[index], change);
388
+ else {
389
+ if (this.pending >= MAX_PENDING) {
390
+ this.overflow();
391
+ return;
392
+ }
393
+ queue.pending.push(change);
394
+ this.pending += 1;
395
+ }
396
+ if (!queue.running && !queue.queued) {
397
+ queue.queued = true;
398
+ this.ready.push(at);
399
+ }
400
+ this.pump();
401
+ }
402
+ pump() {
403
+ while (this.running < MAX_CHATS && this.ready.length > 0) {
404
+ const at = this.ready.shift();
405
+ const queue = this.queues.get(at);
406
+ if (!queue) continue;
407
+ queue.queued = false;
408
+ const change = queue.pending.shift();
409
+ if (!change) {
410
+ this.queues.delete(at);
411
+ continue;
412
+ }
413
+ this.pending -= 1;
414
+ queue.running = true;
415
+ this.running += 1;
416
+ this.handle(change).finally(() => {
417
+ queue.running = false;
418
+ this.running -= 1;
419
+ if (this.queues.get(at) === queue) if (queue.pending.length > 0) {
420
+ queue.queued = true;
421
+ this.ready.push(at);
422
+ } else this.queues.delete(at);
423
+ this.pump();
424
+ });
425
+ }
426
+ }
427
+ overflow() {
428
+ this.logger.warn("chat hub queue full; dropping pending changes", { pending: this.pending });
429
+ for (const [at, queue] of this.queues) {
430
+ queue.pending = [];
431
+ queue.queued = false;
432
+ if (!queue.running) this.queues.delete(at);
433
+ }
434
+ this.ready = [];
435
+ this.pending = 0;
436
+ this.resync([...this.streams]);
437
+ }
438
+ reset() {
439
+ this.logger.info("chat hub reset", { streams: this.streams.size });
440
+ this.resync([...this.streams]);
441
+ }
442
+ resync(streams) {
443
+ for (const stream of streams) {
444
+ stream.push({ type: "resync" });
445
+ this.markDirty(stream.orgId, stream.communityId, stream.orgUserId);
446
+ }
447
+ }
448
+ async handle(change) {
449
+ try {
450
+ switch (change.event) {
451
+ case SENT:
452
+ case UPDATED:
453
+ case DELETED:
454
+ await this.onMessage(change);
455
+ return;
456
+ case REACTIONS:
457
+ await this.onReactions(change);
458
+ return;
459
+ case CREATED:
460
+ case CHAT_UPDATED:
461
+ case ADDED:
462
+ await this.onChat(change);
463
+ return;
464
+ case READ:
465
+ case MUTED:
466
+ if (change.orgUserId) this.markDirty(change.orgId, change.communityId, change.orgUserId, change.chatId);
467
+ return;
468
+ case TYPING:
469
+ await this.onTyping(change);
470
+ return;
471
+ default: this.logger.debug("chat change ignored", { event: change.event });
472
+ }
473
+ } catch (error) {
474
+ this.logger.error("chat change read failed", {
475
+ orgId: change.orgId,
476
+ chatId: change.chatId,
477
+ event: change.event,
478
+ error: describeError(error)
479
+ });
480
+ this.resync(this.communityStreams(change.orgId, change.communityId));
481
+ }
482
+ }
483
+ async participantStreams(change) {
484
+ const active = await this.reads.activeParticipants(change.orgId, change.chatId);
485
+ const members = [];
486
+ const streams = [];
487
+ for (const orgUserId of active) {
488
+ const local = this.streamsOf(change.orgId, change.communityId, orgUserId);
489
+ if (local.length > 0) {
490
+ members.push(orgUserId);
491
+ streams.push(...local);
492
+ }
493
+ }
494
+ return {
495
+ members,
496
+ streams
497
+ };
498
+ }
499
+ async onMessage(change) {
500
+ const { members, streams } = await this.participantStreams(change);
501
+ if (streams.length === 0 || !change.messageId) return;
502
+ const [view] = await this.reads.messageViews(change.orgId, [change.messageId]);
503
+ if (!view || view.chatId !== change.chatId) return;
504
+ const event = view.deletedAt ? {
505
+ type: "message.deleted",
506
+ chatId: change.chatId,
507
+ messageId: view.id,
508
+ deletedAt: view.deletedAt
509
+ } : {
510
+ type: change.event === SENT ? "message.created" : "message.updated",
511
+ chatId: change.chatId,
512
+ message: view
513
+ };
514
+ for (const stream of streams) stream.push(event);
515
+ if (change.event !== UPDATED || view.deletedAt) for (const orgUserId of members) this.markDirty(change.orgId, change.communityId, orgUserId, change.chatId);
516
+ }
517
+ async onReactions(change) {
518
+ const { streams } = await this.participantStreams(change);
519
+ if (streams.length === 0 || !change.messageId || !change.orgUserId) return;
520
+ const reactions = await this.reads.messageReactions(change.orgId, change.messageId);
521
+ for (const stream of streams) stream.push({
522
+ type: "message.reactions",
523
+ chatId: change.chatId,
524
+ messageId: change.messageId,
525
+ reactions,
526
+ orgUserId: change.orgUserId,
527
+ reaction: change.reaction ?? null
528
+ });
529
+ }
530
+ async onChat(change) {
531
+ const live = await this.reads.liveChat(change.orgId, change.chatId);
532
+ const active = new Set(live?.participantIds ?? []);
533
+ if (live) for (const orgUserId of active) {
534
+ const streams = this.streamsOf(change.orgId, change.communityId, orgUserId);
535
+ if (streams.length === 0) continue;
536
+ for (const stream of streams) stream.push({
537
+ type: "chat.updated",
538
+ chatId: change.chatId,
539
+ chat: live.chat,
540
+ participants: live.participants
541
+ });
542
+ this.markDirty(change.orgId, change.communityId, orgUserId, change.chatId);
543
+ }
544
+ for (const orgUserId of change.members ?? []) {
545
+ if (active.has(orgUserId)) continue;
546
+ const streams = this.streamsOf(change.orgId, change.communityId, orgUserId);
547
+ if (streams.length === 0) continue;
548
+ for (const stream of streams) stream.push({
549
+ type: "chat.removed",
550
+ chatId: change.chatId
551
+ });
552
+ this.markDirty(change.orgId, change.communityId, orgUserId);
553
+ }
554
+ }
555
+ async onTyping(change) {
556
+ if (!change.orgUserId || !change.until) return;
557
+ const { streams } = await this.participantStreams(change);
558
+ const until = new Date(change.until);
559
+ for (const stream of streams) {
560
+ if (stream.orgUserId === change.orgUserId) continue;
561
+ stream.push({
562
+ type: "typing",
563
+ chatId: change.chatId,
564
+ orgUserId: change.orgUserId,
565
+ until
566
+ });
567
+ }
568
+ }
569
+ markDirty(orgId, communityId, orgUserId, chatId) {
570
+ const at = key(orgId, communityId);
571
+ const group = this.dirty.get(at) ?? {
572
+ orgId,
573
+ communityId,
574
+ members: /* @__PURE__ */ new Map()
575
+ };
576
+ this.dirty.set(at, group);
577
+ const chats = group.members.get(orgUserId) ?? /* @__PURE__ */ new Set();
578
+ group.members.set(orgUserId, chats);
579
+ if (chatId) chats.add(chatId);
580
+ }
581
+ /** Pushes the `chat.state` of every dirty member and chat and the `unread`
582
+ * of every dirty member, read now. One flush runs at a time. */
583
+ async flush() {
584
+ if (this.flushing) {
585
+ this.flushAgain = true;
586
+ return;
587
+ }
588
+ this.flushing = true;
589
+ try {
590
+ do {
591
+ this.flushAgain = false;
592
+ const groups = [...this.dirty.values()];
593
+ this.dirty = /* @__PURE__ */ new Map();
594
+ await Promise.all(groups.map((group) => this.flushGroup(group)));
595
+ } while (this.flushAgain && this.dirty.size > 0 && !this.closing);
596
+ } finally {
597
+ this.flushing = false;
598
+ }
599
+ }
600
+ async flushGroup(group) {
601
+ const { orgId, communityId } = group;
602
+ const members = [...group.members.keys()].filter((orgUserId) => this.streamsOf(orgId, communityId, orgUserId).length > 0);
603
+ if (members.length === 0) return;
604
+ let states;
605
+ try {
606
+ states = await this.reads.memberStates(orgId, communityId, members);
607
+ } catch (error) {
608
+ this.logger.error("chat member state read failed", {
609
+ orgId,
610
+ communityId,
611
+ error: describeError(error)
612
+ });
613
+ for (const orgUserId of members) {
614
+ const chats = group.members.get(orgUserId) ?? /* @__PURE__ */ new Set();
615
+ this.markDirty(orgId, communityId, orgUserId);
616
+ for (const chatId of chats) this.markDirty(orgId, communityId, orgUserId, chatId);
617
+ }
618
+ return;
619
+ }
620
+ for (const state of states) {
621
+ const chats = group.members.get(state.orgUserId);
622
+ const streams = this.streamsOf(orgId, communityId, state.orgUserId);
623
+ if (!chats || streams.length === 0) continue;
624
+ const events = state.chats.filter((chat) => chats.has(chat.chatId)).map((chat) => ({
625
+ type: "chat.state",
626
+ chatId: chat.chatId,
627
+ unread: chat.unread,
628
+ lastReadMessageId: chat.lastReadMessageId,
629
+ subscription: chat.subscription
630
+ }));
631
+ events.push({
632
+ type: "unread",
633
+ ...state.unread
634
+ });
635
+ for (const stream of streams) for (const event of events) stream.push(event);
636
+ }
637
+ }
638
+ };
639
+ //#endregion
640
+ export { CHAT_CHANNEL, ChatHub, ChatLiveForwarder, TYPING, publishChange };
641
+
642
+ //# sourceMappingURL=live.js.map