@polymorfa/store 0.1.0-dev.20260922094209

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.
@@ -0,0 +1,821 @@
1
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2
+ const str = (value) => typeof value === "string" ? value : undefined;
3
+ const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
4
+ const bool = (value) => typeof value === "boolean" ? value : undefined;
5
+ function identity(value) {
6
+ if (!isRecord(value) || typeof value.id !== "string")
7
+ return undefined;
8
+ return pick({
9
+ id: value.id,
10
+ phoneNumber: str(value.phoneNumber),
11
+ bsuid: str(value.bsuid),
12
+ username: str(value.username),
13
+ });
14
+ }
15
+ function conversationRef(value) {
16
+ const base = identity(value);
17
+ if (base === undefined || !isRecord(value))
18
+ return undefined;
19
+ const sender = identity(value.sender);
20
+ return sender === undefined ? base : { ...base, sender };
21
+ }
22
+ /** Drops `undefined` values so rows stay valid under exact optional types. */
23
+ export function pick(value) {
24
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
25
+ }
26
+ /** Payload times are Unix seconds or milliseconds; returns milliseconds. */
27
+ export function payloadTime(value) {
28
+ const numeric = typeof value === "string" && /^\d+(\.\d+)?$/.test(value)
29
+ ? Number(value)
30
+ : num(value);
31
+ if (numeric !== undefined)
32
+ return numeric < 1e12 ? Math.round(numeric * 1000) : numeric;
33
+ if (typeof value === "string") {
34
+ const parsed = Date.parse(value);
35
+ return Number.isNaN(parsed) ? undefined : parsed;
36
+ }
37
+ return undefined;
38
+ }
39
+ /**
40
+ * Copies each defined field of `patch` onto `row` unless the row already
41
+ * holds a newer value for that field.
42
+ */
43
+ export function applyFields(row, patch, time) {
44
+ const fieldTimes = { ...row.fieldTimes };
45
+ const next = { ...row };
46
+ for (const [field, value] of Object.entries(patch)) {
47
+ if (value === undefined)
48
+ continue;
49
+ if ((fieldTimes[field] ?? -Infinity) > time)
50
+ continue;
51
+ next[field] = value;
52
+ fieldTimes[field] = time;
53
+ }
54
+ next.fieldTimes = fieldTimes;
55
+ next._t = Math.max(row._t, time);
56
+ return next;
57
+ }
58
+ const STATUS_RANK = {
59
+ failed: -1,
60
+ pending: 0,
61
+ sent: 1,
62
+ delivered: 2,
63
+ read: 3,
64
+ played: 4,
65
+ };
66
+ /** Status only moves forward, whatever order acknowledgements arrive in. */
67
+ export function advanceStatus(current, next) {
68
+ if (current === "failed")
69
+ return next === "pending" ? current : next;
70
+ return STATUS_RANK[next] > STATUS_RANK[current] ? next : current;
71
+ }
72
+ function ackStatus(type) {
73
+ const normalized = type.toLowerCase();
74
+ if (normalized.includes("play"))
75
+ return "played";
76
+ if (normalized.includes("read"))
77
+ return "read";
78
+ if (normalized.includes("deliver"))
79
+ return "delivered";
80
+ if (normalized === "sent" || normalized === "server")
81
+ return "sent";
82
+ return undefined;
83
+ }
84
+ function emptyMessage(id, session, conversationId, time) {
85
+ return {
86
+ id,
87
+ _t: time,
88
+ session,
89
+ conversationId,
90
+ createdAt: time,
91
+ fromMe: false,
92
+ status: "sent",
93
+ stub: true,
94
+ };
95
+ }
96
+ function emptyConversation(id, time) {
97
+ return { id, _t: time, lastActivity: 0, unreadCount: 0 };
98
+ }
99
+ function preview(message) {
100
+ return message.text ?? message.caption ?? "";
101
+ }
102
+ /** Upserts message content and updates the conversation summary. */
103
+ export async function upsertMessage(context, patch, time, options) {
104
+ const conversation = (await context.get("conversations", patch.conversationId)) ??
105
+ emptyConversation(patch.conversationId, time);
106
+ const createdAt = patch.createdAt ?? time;
107
+ if (conversation.clearedAt !== undefined &&
108
+ createdAt <= conversation.clearedAt)
109
+ return;
110
+ const existing = await context.get("messages", patch.id);
111
+ if (existing?.deleted === true)
112
+ return;
113
+ const base = existing ??
114
+ emptyMessage(patch.id, patch.session, patch.conversationId, createdAt);
115
+ // `createdAt` and `conversationId` are identity, not state: always accept.
116
+ let next = applyFields(base, patch, time);
117
+ if (patch.createdAt !== undefined)
118
+ next = { ...next, createdAt };
119
+ if (options.content)
120
+ next = { ...next, stub: false };
121
+ if (next.stub !== true) {
122
+ const rest = { ...next };
123
+ delete rest.stub;
124
+ next = rest;
125
+ }
126
+ if (options.status !== undefined)
127
+ next = {
128
+ ...next,
129
+ status: advanceStatus(existing?.status ?? "pending", options.status),
130
+ };
131
+ else if (existing !== undefined)
132
+ next = { ...next, status: existing.status };
133
+ context.put("messages", next);
134
+ if (next.stub === true)
135
+ return;
136
+ let summary = conversation;
137
+ if (createdAt >= conversation.lastActivity) {
138
+ summary = {
139
+ ...summary,
140
+ lastActivity: createdAt,
141
+ lastMessage: pick({
142
+ id: next.id,
143
+ text: preview(next),
144
+ type: next.type,
145
+ fromMe: next.fromMe,
146
+ createdAt,
147
+ }),
148
+ };
149
+ }
150
+ const isNewInbound = (existing === undefined || existing.stub === true) &&
151
+ !next.fromMe &&
152
+ createdAt > (conversation.readAt ?? -Infinity);
153
+ if (isNewInbound)
154
+ summary = { ...summary, unreadCount: summary.unreadCount + 1 };
155
+ if (!next.fromMe && next.pushName !== undefined && summary.isGroup !== true)
156
+ summary = applyFields(summary, { name: next.pushName }, time);
157
+ if (summary !== conversation || existing === undefined)
158
+ context.put("conversations", {
159
+ ...summary,
160
+ _t: Math.max(summary._t, time),
161
+ ...(conversation.session === undefined ? { session: patch.session } : {}),
162
+ });
163
+ }
164
+ async function tombstone(context, id, conversationId, event, time, revoked) {
165
+ const existing = await context.get("messages", id);
166
+ const conversation = existing?.conversationId ?? conversationId;
167
+ if (conversation === undefined)
168
+ return;
169
+ const base = existing ?? emptyMessage(id, event.session, conversation, time);
170
+ const row = {
171
+ ...base,
172
+ _t: Math.max(base._t, time),
173
+ deleted: true,
174
+ ...(revoked ? { revoked: true } : {}),
175
+ };
176
+ delete row.text;
177
+ delete row.caption;
178
+ delete row.attachments;
179
+ delete row.mediaUrl;
180
+ delete row.reactions;
181
+ context.put("messages", row);
182
+ }
183
+ function messageFromPayload(event, payload, time) {
184
+ const id = str(payload.id);
185
+ const conversation = conversationRef(payload.conversation);
186
+ if (id === undefined || conversation === undefined)
187
+ return undefined;
188
+ const mediaUrl = str(payload.mediaUrl);
189
+ const mimeType = str(payload.mimeType);
190
+ const filename = str(payload.filename);
191
+ const type = str(payload.type);
192
+ return pick({
193
+ id,
194
+ session: event.session,
195
+ conversationId: conversation.id,
196
+ createdAt: payloadTime(payload.timestamp) ?? time,
197
+ fromMe: bool(payload.fromMe) ?? false,
198
+ senderId: conversation.sender?.id,
199
+ pushName: str(payload.pushName) ?? str(payload.senderName),
200
+ type,
201
+ text: str(payload.text),
202
+ caption: str(payload.caption),
203
+ mimeType,
204
+ filename,
205
+ mediaUrl,
206
+ edited: bool(payload.edited),
207
+ attachments: mediaUrl === undefined
208
+ ? undefined
209
+ : [
210
+ pick({
211
+ id: str(payload.media) ?? id,
212
+ name: filename ?? type ?? "attachment",
213
+ size: 0,
214
+ contentType: mimeType ?? "application/octet-stream",
215
+ url: mediaUrl,
216
+ }),
217
+ ],
218
+ });
219
+ }
220
+ const receivedMessage = async (event, context) => {
221
+ if (!isRecord(event.payload))
222
+ return false;
223
+ const payload = event.payload;
224
+ if (typeof payload.revokedId === "string" && payload.type === "revoke") {
225
+ await tombstone(context, payload.revokedId, conversationRef(payload.conversation)?.id, event, context.time, true);
226
+ return;
227
+ }
228
+ const message = messageFromPayload(event, payload, context.time);
229
+ if (message === undefined)
230
+ return false;
231
+ await upsertMessage(context, message, context.time, {
232
+ content: true,
233
+ ...(message.fromMe === true ? { status: "sent" } : {}),
234
+ });
235
+ };
236
+ const reactionMessage = async (event, context) => {
237
+ if (!isRecord(event.payload))
238
+ return false;
239
+ const payload = event.payload;
240
+ const target = str(payload.reactionTo);
241
+ const conversation = conversationRef(payload.conversation);
242
+ if (target === undefined || conversation === undefined)
243
+ return false;
244
+ const reactor = bool(payload.fromMe) === true
245
+ ? "me"
246
+ : (conversation.sender?.id ?? conversation.id);
247
+ const existing = (await context.get("messages", target)) ??
248
+ emptyMessage(target, event.session, conversation.id, context.time);
249
+ if (existing.deleted === true)
250
+ return;
251
+ const key = `reaction:${reactor}`;
252
+ if ((existing.fieldTimes?.[key] ?? -Infinity) > context.time)
253
+ return;
254
+ const reactions = { ...existing.reactions };
255
+ const reaction = str(payload.reaction) ?? "";
256
+ if (reaction === "")
257
+ delete reactions[reactor];
258
+ else
259
+ reactions[reactor] = reaction;
260
+ context.put("messages", {
261
+ ...existing,
262
+ reactions,
263
+ fieldTimes: { ...existing.fieldTimes, [key]: context.time },
264
+ _t: Math.max(existing._t, context.time),
265
+ });
266
+ };
267
+ const sentMessage = async (event, context) => {
268
+ if (!isRecord(event.payload))
269
+ return false;
270
+ const payload = event.payload;
271
+ const id = str(payload.id);
272
+ const conversation = conversationRef(payload.conversation);
273
+ if (id === undefined || conversation === undefined)
274
+ return false;
275
+ await upsertMessage(context, pick({
276
+ id,
277
+ session: event.session,
278
+ conversationId: conversation.id,
279
+ createdAt: payloadTime(payload.timestamp) ?? context.time,
280
+ fromMe: true,
281
+ type: str(payload.type),
282
+ }), context.time, { content: true, status: "sent" });
283
+ };
284
+ const ackMessage = async (event, context) => {
285
+ if (!isRecord(event.payload))
286
+ return false;
287
+ const payload = event.payload;
288
+ const conversation = conversationRef(payload.conversation);
289
+ const type = str(payload.type) ?? "";
290
+ const status = ackStatus(type);
291
+ if (conversation === undefined || !Array.isArray(payload.messages))
292
+ return false;
293
+ for (const entry of payload.messages) {
294
+ const id = isRecord(entry) ? str(entry.id) : undefined;
295
+ if (id === undefined)
296
+ continue;
297
+ const existing = (await context.get("messages", id)) ??
298
+ emptyMessage(id, event.session, conversation.id, payloadTime(payload.timestamp) ?? context.time);
299
+ if (existing.deleted === true)
300
+ continue;
301
+ context.put("messages", {
302
+ ...existing,
303
+ status: status === undefined
304
+ ? existing.status
305
+ : advanceStatus(existing.status, status),
306
+ ...(status === undefined ? { ackType: type } : {}),
307
+ _t: Math.max(existing._t, context.time),
308
+ });
309
+ }
310
+ };
311
+ const deleteMessage = async (event, context) => {
312
+ if (!isRecord(event.payload))
313
+ return false;
314
+ const id = str(event.payload.id);
315
+ if (id === undefined)
316
+ return false;
317
+ await tombstone(context, id, conversationRef(event.payload.conversation)?.id, event, context.time, false);
318
+ };
319
+ const revokedMessage = async (event, context) => {
320
+ if (!isRecord(event.payload))
321
+ return false;
322
+ const payload = event.payload;
323
+ const id = str(payload.revokedId) ?? str(payload.id);
324
+ if (id === undefined)
325
+ return false;
326
+ await tombstone(context, id, conversationRef(payload.conversation)?.id, event, context.time, true);
327
+ };
328
+ const voteMessage = async (event, context) => {
329
+ if (!isRecord(event.payload))
330
+ return false;
331
+ const payload = event.payload;
332
+ const target = str(payload.pollMessageId);
333
+ const voter = identity(payload.voter);
334
+ const conversation = conversationRef(payload.conversation);
335
+ if (target === undefined || voter === undefined || conversation === undefined)
336
+ return false;
337
+ const selected = Array.isArray(payload.selectedHashes)
338
+ ? payload.selectedHashes.filter((hash) => typeof hash === "string")
339
+ : [];
340
+ const time = payloadTime(payload.timestamp) ?? context.time;
341
+ const existing = (await context.get("messages", target)) ??
342
+ emptyMessage(target, event.session, conversation.id, time);
343
+ const key = `vote:${voter.id}`;
344
+ if ((existing.fieldTimes?.[key] ?? -Infinity) > time)
345
+ return;
346
+ context.put("messages", {
347
+ ...existing,
348
+ votes: { ...existing.votes, [voter.id]: selected },
349
+ fieldTimes: { ...existing.fieldTimes, [key]: time },
350
+ _t: Math.max(existing._t, context.time),
351
+ });
352
+ };
353
+ async function updateConversation(context, id, patch, event) {
354
+ const existing = (await context.get("conversations", id)) ?? {
355
+ ...emptyConversation(id, context.time),
356
+ session: event.session,
357
+ };
358
+ const next = applyFields(existing, patch, context.time);
359
+ context.put("conversations", next);
360
+ return next;
361
+ }
362
+ function chatId(event) {
363
+ return isRecord(event.payload) ? identity(event.payload.from)?.id : undefined;
364
+ }
365
+ const chatRead = async (event, context) => {
366
+ const id = chatId(event);
367
+ if (id === undefined || !isRecord(event.payload))
368
+ return false;
369
+ const read = bool(event.payload.read);
370
+ if (read === undefined)
371
+ return false;
372
+ const existing = await context.get("conversations", id);
373
+ if (read) {
374
+ if ((existing?.readAt ?? -Infinity) > context.time)
375
+ return;
376
+ await updateConversation(context, id, { readAt: context.time, markedUnread: false }, event);
377
+ const updated = await context.get("conversations", id);
378
+ if (updated !== undefined)
379
+ context.put("conversations", { ...updated, unreadCount: 0 });
380
+ }
381
+ else {
382
+ await updateConversation(context, id, { markedUnread: true }, event);
383
+ }
384
+ };
385
+ const chatArchive = async (event, context) => {
386
+ const id = chatId(event);
387
+ if (id === undefined || !isRecord(event.payload))
388
+ return false;
389
+ await updateConversation(context, id, pick({
390
+ archived: bool(event.payload.archive),
391
+ pinned: bool(event.payload.pinned),
392
+ }), event);
393
+ };
394
+ const chatMute = async (event, context) => {
395
+ const id = chatId(event);
396
+ if (id === undefined || !isRecord(event.payload))
397
+ return false;
398
+ await updateConversation(context, id, pick({
399
+ muted: bool(event.payload.muted),
400
+ muteEndTimestamp: num(event.payload.muteEndTimestamp),
401
+ }), event);
402
+ };
403
+ function clearChat(deleted) {
404
+ return async (event, context) => {
405
+ const id = chatId(event);
406
+ if (id === undefined)
407
+ return false;
408
+ const existing = await context.get("conversations", id);
409
+ if ((existing?.clearedAt ?? -Infinity) >= context.time && !deleted)
410
+ return;
411
+ context.deleteConversationMessages(id);
412
+ const base = existing ?? {
413
+ ...emptyConversation(id, context.time),
414
+ session: event.session,
415
+ };
416
+ const row = {
417
+ ...base,
418
+ _t: Math.max(base._t, context.time),
419
+ clearedAt: Math.max(base.clearedAt ?? -Infinity, context.time),
420
+ unreadCount: 0,
421
+ ...(deleted ? { deleted: true } : {}),
422
+ };
423
+ delete row.lastMessage;
424
+ context.put("conversations", row);
425
+ };
426
+ }
427
+ const contactUpdate = async (event, context) => {
428
+ if (!isRecord(event.payload))
429
+ return false;
430
+ const payload = event.payload;
431
+ const id = str(payload.id);
432
+ if (id === undefined)
433
+ return false;
434
+ const existing = (await context.get("contacts", id)) ?? {
435
+ id,
436
+ _t: context.time,
437
+ };
438
+ context.put("contacts", applyFields(existing, pick({
439
+ phoneNumber: str(payload.phoneNumber),
440
+ bsuid: str(payload.bsuid),
441
+ username: str(payload.username),
442
+ fullName: str(payload.fullName),
443
+ firstName: str(payload.firstName),
444
+ pushName: str(payload.pushName),
445
+ businessName: str(payload.businessName),
446
+ pictureId: str(payload.pictureId),
447
+ pictureRemoved: bool(payload.pictureRemoved),
448
+ }), context.time));
449
+ };
450
+ const blocklistUpdate = async (event, context) => {
451
+ if (!isRecord(event.payload) || !Array.isArray(event.payload.changes))
452
+ return false;
453
+ for (const change of event.payload.changes) {
454
+ if (!isRecord(change))
455
+ continue;
456
+ const id = str(change.id);
457
+ const action = str(change.action)?.toLowerCase();
458
+ if (id === undefined || (action !== "block" && action !== "unblock"))
459
+ continue;
460
+ const existing = (await context.get("contacts", id)) ?? {
461
+ id,
462
+ _t: context.time,
463
+ };
464
+ context.put("contacts", applyFields(existing, pick({
465
+ blocked: action === "block",
466
+ phoneNumber: str(change.phoneNumber),
467
+ bsuid: str(change.bsuid),
468
+ username: str(change.username),
469
+ }), context.time));
470
+ }
471
+ };
472
+ const presenceUpdate = async (event, context) => {
473
+ if (!isRecord(event.payload))
474
+ return false;
475
+ const payload = event.payload;
476
+ const who = identity(payload.from) ?? identity(payload.sender);
477
+ if (who === undefined)
478
+ return false;
479
+ const observedAt = payloadTime(payload.observedAt) ?? context.time;
480
+ const existing = await context.get("presence", who.id);
481
+ if (existing !== undefined && existing.observedAt > observedAt)
482
+ return;
483
+ context.put("presence", pick({
484
+ id: who.id,
485
+ _t: observedAt,
486
+ observedAt,
487
+ state: str(payload.state),
488
+ media: str(payload.media),
489
+ unavailable: bool(payload.unavailable),
490
+ lastSeen: payloadTime(payload.lastSeen),
491
+ }));
492
+ };
493
+ const CALL_RANK = {
494
+ ringing: 0,
495
+ active: 1,
496
+ rejected: 2,
497
+ missed: 2,
498
+ ended: 2,
499
+ };
500
+ function callEvent(state) {
501
+ return async (event, context) => {
502
+ if (!isRecord(event.payload))
503
+ return false;
504
+ const payload = event.payload;
505
+ const callId = str(payload.callId);
506
+ if (callId === undefined)
507
+ return false;
508
+ const existing = (await context.get("calls", callId)) ?? {
509
+ id: callId,
510
+ _t: context.time,
511
+ session: event.session,
512
+ state: "ringing",
513
+ startedAt: context.time,
514
+ };
515
+ const from = identity(payload.from);
516
+ let next = applyFields(existing, pick({
517
+ from,
518
+ direction: payload.direction === "inbound" || payload.direction === "outbound"
519
+ ? payload.direction
520
+ : (existing.direction ??
521
+ (event.event === "call.received" ? "inbound" : undefined)),
522
+ reason: str(payload.reason),
523
+ durationSeconds: num(payload.durationSeconds),
524
+ hadVideo: bool(payload.hadVideo),
525
+ }), context.time);
526
+ if (state !== undefined) {
527
+ const stateAt = existing.fieldTimes?.state;
528
+ const incoming = CALL_RANK[state];
529
+ const current = CALL_RANK[existing.state];
530
+ const accept = stateAt === undefined
531
+ ? incoming >= current
532
+ : incoming > current ||
533
+ (incoming === current && incoming < 2 && context.time >= stateAt);
534
+ if (accept)
535
+ next = {
536
+ ...next,
537
+ state,
538
+ fieldTimes: { ...next.fieldTimes, state: context.time },
539
+ };
540
+ if (state === "ringing" && context.time < next.startedAt)
541
+ next = { ...next, startedAt: context.time };
542
+ if (state === "active" && next.acceptedAt === undefined)
543
+ next = { ...next, acceptedAt: context.time };
544
+ if (CALL_RANK[state] === 2 && next.endedAt === undefined)
545
+ next = { ...next, endedAt: context.time };
546
+ }
547
+ context.put("calls", next);
548
+ };
549
+ }
550
+ const callParticipant = async (event, context) => {
551
+ if (!isRecord(event.payload))
552
+ return false;
553
+ const payload = event.payload;
554
+ const callId = str(payload.callId);
555
+ if (callId === undefined)
556
+ return false;
557
+ const existing = (await context.get("calls", callId)) ?? {
558
+ id: callId,
559
+ _t: context.time,
560
+ session: event.session,
561
+ state: "active",
562
+ startedAt: context.time,
563
+ };
564
+ const participant = isRecord(payload.participant)
565
+ ? payload.participant
566
+ : undefined;
567
+ const participantId = str(participant?.id) ?? str(payload.participantId);
568
+ if (participantId === undefined)
569
+ return false;
570
+ const key = `participant:${participantId}`;
571
+ if ((existing.fieldTimes?.[key] ?? -Infinity) > context.time)
572
+ return;
573
+ const previous = existing.participants?.[participantId];
574
+ const updated = pick({
575
+ id: participantId,
576
+ state: event.event === "call.participant_left"
577
+ ? "left"
578
+ : (str(participant?.state) ?? previous?.state ?? "connected"),
579
+ phoneNumber: str(participant?.phoneNumber) ?? previous?.phoneNumber,
580
+ username: str(participant?.username) ?? previous?.username,
581
+ reason: str(payload.reason),
582
+ });
583
+ context.put("calls", {
584
+ ...existing,
585
+ participants: { ...existing.participants, [participantId]: updated },
586
+ fieldTimes: { ...existing.fieldTimes, [key]: context.time },
587
+ _t: Math.max(existing._t, context.time),
588
+ });
589
+ };
590
+ const callTelemetry = async (event, context) => {
591
+ if (!isRecord(event.payload))
592
+ return false;
593
+ const callId = str(event.payload.callId);
594
+ if (callId === undefined)
595
+ return false;
596
+ const existing = (await context.get("calls", callId)) ?? {
597
+ id: callId,
598
+ _t: context.time,
599
+ session: event.session,
600
+ state: "ended",
601
+ startedAt: context.time,
602
+ };
603
+ context.put("calls", applyFields(existing, { telemetry: event.payload }, context.time));
604
+ };
605
+ const labelsUpdate = async (event, context) => {
606
+ if (!isRecord(event.payload))
607
+ return false;
608
+ const payload = event.payload;
609
+ const action = str(payload.action);
610
+ const labelId = str(payload.labelId);
611
+ const messageId = str(payload.messageId);
612
+ if (action === "label_edit" && labelId !== undefined) {
613
+ const existing = (await context.get("labels", labelId)) ?? {
614
+ id: labelId,
615
+ _t: context.time,
616
+ };
617
+ context.put("labels", applyFields(existing, pick({
618
+ name: str(payload.name) ?? str(payload.label),
619
+ color: num(payload.color),
620
+ orderIndex: num(payload.orderIndex),
621
+ deleted: bool(payload.deleted),
622
+ }), context.time));
623
+ return;
624
+ }
625
+ if (action === "label_association_chat" && labelId !== undefined) {
626
+ const id = identity(payload.from)?.id;
627
+ const labeled = bool(payload.labeled);
628
+ if (id === undefined || labeled === undefined)
629
+ return false;
630
+ const existing = (await context.get("conversations", id)) ?? {
631
+ ...emptyConversation(id, context.time),
632
+ session: event.session,
633
+ };
634
+ context.put("conversations", toggleLabel(existing, labelId, labeled, context.time));
635
+ return;
636
+ }
637
+ if (action === "label_association_message" &&
638
+ labelId !== undefined &&
639
+ messageId !== undefined) {
640
+ const labeled = bool(payload.labeled);
641
+ const existing = await context.get("messages", messageId);
642
+ if (existing === undefined || labeled === undefined)
643
+ return false;
644
+ context.put("messages", toggleLabel(existing, labelId, labeled, context.time));
645
+ return;
646
+ }
647
+ if (action === "star" && messageId !== undefined) {
648
+ const starred = bool(payload.starred);
649
+ const existing = await context.get("messages", messageId);
650
+ if (existing === undefined || starred === undefined)
651
+ return false;
652
+ context.put("messages", applyFields(existing, { starred }, context.time));
653
+ return;
654
+ }
655
+ return false;
656
+ };
657
+ function toggleLabel(row, labelId, labeled, time) {
658
+ const key = `label:${labelId}`;
659
+ if ((row.fieldTimes?.[key] ?? -Infinity) > time)
660
+ return row;
661
+ const labels = new Set(row.labelIds ?? []);
662
+ if (labeled)
663
+ labels.add(labelId);
664
+ else
665
+ labels.delete(labelId);
666
+ return {
667
+ ...row,
668
+ labelIds: [...labels],
669
+ fieldTimes: { ...row.fieldTimes, [key]: time },
670
+ _t: Math.max(row._t, time),
671
+ };
672
+ }
673
+ function sessionEvent(patch) {
674
+ return async (event, context) => {
675
+ if (!isRecord(event.payload))
676
+ return false;
677
+ const existing = (await context.get("sessions", event.session)) ?? {
678
+ id: event.session,
679
+ _t: context.time,
680
+ };
681
+ context.put("sessions", applyFields(existing, pick(patch(event.payload)), context.time));
682
+ };
683
+ }
684
+ const templateStatus = async (event, context) => {
685
+ if (!isRecord(event.payload))
686
+ return false;
687
+ const payload = event.payload;
688
+ const id = str(payload.templateId);
689
+ if (id === undefined)
690
+ return false;
691
+ const existing = (await context.get("templates", id)) ?? {
692
+ id,
693
+ _t: context.time,
694
+ };
695
+ context.put("templates", applyFields(existing, pick({
696
+ templateName: str(payload.templateName),
697
+ status: str(payload.status),
698
+ category: str(payload.category),
699
+ reason: str(payload.reason),
700
+ qualityRating: str(payload.qualityRating),
701
+ }), context.time));
702
+ };
703
+ const groupUpdate = async (event, context) => {
704
+ if (!isRecord(event.payload))
705
+ return false;
706
+ const id = str(event.payload.id);
707
+ if (id === undefined)
708
+ return false;
709
+ await updateConversation(context, id, pick({
710
+ isGroup: true,
711
+ name: str(event.payload.newSubject),
712
+ description: str(event.payload.newDescription),
713
+ }), event);
714
+ };
715
+ export const BUILT_IN_REDUCERS = {
716
+ "message.received": receivedMessage,
717
+ "message.update": receivedMessage,
718
+ "message.edited": receivedMessage,
719
+ "message.reaction": reactionMessage,
720
+ "message.sent": sentMessage,
721
+ "message.ack": ackMessage,
722
+ "message.delete": deleteMessage,
723
+ "message.revoked": revokedMessage,
724
+ "message.vote": voteMessage,
725
+ "chat.read": chatRead,
726
+ "chat.archive": chatArchive,
727
+ "chat.mute": chatMute,
728
+ "chat.clear": clearChat(false),
729
+ "chat.delete": clearChat(true),
730
+ "contact.update": contactUpdate,
731
+ "blocklist.update": blocklistUpdate,
732
+ "presence.update": presenceUpdate,
733
+ "call.received": callEvent("ringing"),
734
+ "call.accepted": callEvent("active"),
735
+ "call.rejected": callEvent("rejected"),
736
+ "call.missed": callEvent("missed"),
737
+ "call.ended": callEvent("ended"),
738
+ "call.participant_joined": callParticipant,
739
+ "call.participant_state": callParticipant,
740
+ "call.participant_left": callParticipant,
741
+ "call.telemetry": callTelemetry,
742
+ "labels.update": labelsUpdate,
743
+ "session.status": sessionEvent((payload) => ({
744
+ status: str(payload.status),
745
+ statusReason: str(payload.statusReason),
746
+ })),
747
+ "session.connected": sessionEvent((payload) => ({
748
+ status: "connected",
749
+ phoneNumber: str(payload.phoneNumber),
750
+ pushName: str(payload.pushName),
751
+ phonePlatform: str(payload.phonePlatform),
752
+ accountType: str(payload.accountType),
753
+ businessName: str(payload.businessName),
754
+ })),
755
+ "session.logged_out": sessionEvent((payload) => ({
756
+ status: "logged_out",
757
+ loggedOutReason: str(payload.reason),
758
+ })),
759
+ "session.phone_offline": sessionEvent((payload) => ({
760
+ phoneOffline: {
761
+ daysSinceLastSeen: num(payload.daysSinceLastSeen) ?? 0,
762
+ daysRemaining: num(payload.daysRemaining) ?? 0,
763
+ lastSeen: str(payload.lastSeen) ?? "",
764
+ action: str(payload.action) ?? "",
765
+ },
766
+ })),
767
+ "template.status": templateStatus,
768
+ "group.update": groupUpdate,
769
+ };
770
+ /** Keys the built-in reducers read, so a batch can load them together. */
771
+ export function keyHints(event) {
772
+ if (!isRecord(event.payload))
773
+ return [];
774
+ const payload = event.payload;
775
+ const hints = [];
776
+ const add = (store, key) => {
777
+ if (typeof key === "string")
778
+ hints.push([store, key]);
779
+ };
780
+ const conversation = conversationRef(payload.conversation);
781
+ if (conversation !== undefined)
782
+ add("conversations", conversation.id);
783
+ if (event.event.startsWith("message.")) {
784
+ add("messages", payload.id);
785
+ add("messages", payload.reactionTo);
786
+ add("messages", payload.revokedId);
787
+ add("messages", payload.pollMessageId);
788
+ if (Array.isArray(payload.messages))
789
+ for (const entry of payload.messages)
790
+ if (isRecord(entry))
791
+ add("messages", entry.id);
792
+ }
793
+ else if (event.event.startsWith("chat.")) {
794
+ add("conversations", identity(payload.from)?.id);
795
+ }
796
+ else if (event.event.startsWith("call.")) {
797
+ add("calls", payload.callId);
798
+ }
799
+ else if (event.event === "contact.update") {
800
+ add("contacts", payload.id);
801
+ }
802
+ else if (event.event === "presence.update") {
803
+ add("presence", (identity(payload.from) ?? identity(payload.sender))?.id);
804
+ }
805
+ else if (event.event.startsWith("session.")) {
806
+ add("sessions", event.session);
807
+ }
808
+ else if (event.event === "template.status") {
809
+ add("templates", payload.templateId);
810
+ }
811
+ else if (event.event === "labels.update") {
812
+ add("labels", payload.labelId);
813
+ add("messages", payload.messageId);
814
+ add("conversations", identity(payload.from)?.id);
815
+ }
816
+ else if (event.event === "group.update") {
817
+ add("conversations", payload.id);
818
+ }
819
+ return hints;
820
+ }
821
+ //# sourceMappingURL=reducers.js.map