@open-mercato/core 0.10.1-develop.7768.1.90fa052c11 → 0.10.1-develop.7770.1.970b396ebb

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.
@@ -268,9 +268,13 @@ export type CreateFeatureNotificationServiceInput = CreateFeatureNotificationInp
268
268
  restrictRecipientsToOrganization?: boolean
269
269
  }
270
270
 
271
+ export type CreateBatchNotificationOptions = {
272
+ skipExistingGroup?: boolean
273
+ }
274
+
271
275
  export interface NotificationService {
272
276
  create(input: CreateNotificationInput, ctx: NotificationServiceContext): Promise<Notification>
273
- createBatch(input: CreateBatchNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>
277
+ createBatch(input: CreateBatchNotificationInput, ctx: NotificationServiceContext, options?: CreateBatchNotificationOptions): Promise<Notification[]>
274
278
  createForRole(input: CreateRoleNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>
275
279
  createForFeature(input: CreateFeatureNotificationServiceInput, ctx: NotificationServiceContext): Promise<Notification[]>
276
280
  markAsRead(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>
@@ -436,8 +440,12 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
436
440
  return notification
437
441
  },
438
442
 
439
- async createBatch(input, ctx) {
443
+ async createBatch(input, ctx, options) {
444
+ if (options?.skipExistingGroup && !input.groupKey?.trim()) {
445
+ throw new Error('[internal] Notification group key is required for deduplicated batch delivery')
446
+ }
440
447
  const recipientUserIds = Array.from(new Set(input.recipientUserIds))
448
+ if (options?.skipExistingGroup) recipientUserIds.sort((left, right) => left.localeCompare(right))
441
449
  const { recipientUserIds: _recipientUserIds, ...content } = input
442
450
  const notifications: Notification[] = []
443
451
  const resolved = await resolveChannelsForRecipients(content, recipientUserIds, ctx)
@@ -446,12 +454,26 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
446
454
  await writeEm.transactional(async (tx) => {
447
455
  await assertNotificationRecipientsInScope(tx, recipientUserIds, ctx)
448
456
  for (const { recipientUserId, channels } of resolved) {
457
+ if (options?.skipExistingGroup) {
458
+ const organizationId = normalizeOrgScope(ctx.organizationId)
459
+ const lockKey = `notifications:${ctx.tenantId}:${organizationId ?? 'global'}:${recipientUserId}:${content.type}:${content.groupKey}`
460
+ await sql`select pg_advisory_xact_lock(hashtext(${lockKey}))`.execute(getDb(tx))
461
+ const existing = await tx.findOne(Notification, {
462
+ recipientUserId,
463
+ tenantId: ctx.tenantId,
464
+ organizationId,
465
+ type: content.type,
466
+ groupKey: content.groupKey,
467
+ })
468
+ if (existing) continue
469
+ }
449
470
  const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)
450
471
  notifications.push(notification)
451
472
  }
452
473
  await tx.flush()
453
474
  })
454
475
 
476
+ if (options?.skipExistingGroup && notifications.length === 0) return []
455
477
  await invalidateNotificationCache(container, ctx, 'created')
456
478
  await emitNotificationCreatedBatch(eventBus, notifications, ctx)
457
479
  await emitNotificationSseEvents(eventBus, notifications, ctx)