@ttt-productions/notification-core 0.15.16 → 0.15.18

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 (98) hide show
  1. package/README.md +9 -10
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +0 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/react/components/NotificationEmptyState.d.ts +0 -3
  7. package/dist/react/components/NotificationEmptyState.d.ts.map +1 -1
  8. package/dist/react/components/NotificationEmptyState.js +0 -3
  9. package/dist/react/components/NotificationEmptyState.js.map +1 -1
  10. package/dist/react/components/NotificationHistoryList.d.ts +1 -7
  11. package/dist/react/components/NotificationHistoryList.d.ts.map +1 -1
  12. package/dist/react/components/NotificationHistoryList.js +4 -9
  13. package/dist/react/components/NotificationHistoryList.js.map +1 -1
  14. package/dist/react/components/NotificationList.d.ts +1 -4
  15. package/dist/react/components/NotificationList.d.ts.map +1 -1
  16. package/dist/react/components/NotificationList.js +26 -38
  17. package/dist/react/components/NotificationList.js.map +1 -1
  18. package/dist/react/components/NotificationUnreadBadge.d.ts +0 -5
  19. package/dist/react/components/NotificationUnreadBadge.d.ts.map +1 -1
  20. package/dist/react/components/NotificationUnreadBadge.js +1 -6
  21. package/dist/react/components/NotificationUnreadBadge.js.map +1 -1
  22. package/dist/react/components/relative-time.d.ts +0 -3
  23. package/dist/react/components/relative-time.d.ts.map +1 -1
  24. package/dist/react/components/relative-time.js +0 -3
  25. package/dist/react/components/relative-time.js.map +1 -1
  26. package/dist/react/hooks/useActiveNotifications.d.ts +3 -7
  27. package/dist/react/hooks/useActiveNotifications.d.ts.map +1 -1
  28. package/dist/react/hooks/useActiveNotifications.js +3 -8
  29. package/dist/react/hooks/useActiveNotifications.js.map +1 -1
  30. package/dist/react/hooks/useArchiveAllNotifications.d.ts +0 -30
  31. package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
  32. package/dist/react/hooks/useArchiveAllNotifications.js +15 -45
  33. package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
  34. package/dist/react/hooks/useArchiveNotification.d.ts +0 -8
  35. package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
  36. package/dist/react/hooks/useArchiveNotification.js +0 -8
  37. package/dist/react/hooks/useArchiveNotification.js.map +1 -1
  38. package/dist/react/hooks/useNotificationHistory.d.ts +3 -17
  39. package/dist/react/hooks/useNotificationHistory.d.ts.map +1 -1
  40. package/dist/react/hooks/useNotificationHistory.js +3 -20
  41. package/dist/react/hooks/useNotificationHistory.js.map +1 -1
  42. package/dist/react/hooks/useUnreadCount.d.ts +0 -22
  43. package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
  44. package/dist/react/hooks/useUnreadCount.js +2 -25
  45. package/dist/react/hooks/useUnreadCount.js.map +1 -1
  46. package/dist/react/index.d.ts +10 -2
  47. package/dist/react/index.d.ts.map +1 -1
  48. package/dist/react/index.js +10 -5
  49. package/dist/react/index.js.map +1 -1
  50. package/dist/react/types.d.ts +30 -0
  51. package/dist/react/types.d.ts.map +1 -0
  52. package/dist/react/types.js +2 -0
  53. package/dist/react/types.js.map +1 -0
  54. package/dist/schemas.d.ts +0 -22
  55. package/dist/schemas.d.ts.map +1 -1
  56. package/dist/schemas.js +0 -25
  57. package/dist/schemas.js.map +1 -1
  58. package/dist/server/activeNotificationId.d.ts +1 -22
  59. package/dist/server/activeNotificationId.d.ts.map +1 -1
  60. package/dist/server/activeNotificationId.js +0 -16
  61. package/dist/server/activeNotificationId.js.map +1 -1
  62. package/dist/server/delivery-ledger.d.ts +1 -73
  63. package/dist/server/delivery-ledger.d.ts.map +1 -1
  64. package/dist/server/delivery-ledger.js +13 -63
  65. package/dist/server/delivery-ledger.js.map +1 -1
  66. package/dist/server/errors.d.ts +0 -9
  67. package/dist/server/errors.d.ts.map +1 -1
  68. package/dist/server/errors.js +1 -11
  69. package/dist/server/errors.js.map +1 -1
  70. package/dist/server/index.d.ts.map +1 -1
  71. package/dist/server/index.js +0 -3
  72. package/dist/server/index.js.map +1 -1
  73. package/dist/server/observed-generation.d.ts +1 -41
  74. package/dist/server/observed-generation.d.ts.map +1 -1
  75. package/dist/server/observed-generation.js +1 -31
  76. package/dist/server/observed-generation.js.map +1 -1
  77. package/dist/server/processBatchHelper.d.ts +0 -19
  78. package/dist/server/processBatchHelper.d.ts.map +1 -1
  79. package/dist/server/processBatchHelper.js +5 -51
  80. package/dist/server/processBatchHelper.js.map +1 -1
  81. package/dist/server/types.d.ts +0 -9
  82. package/dist/server/types.d.ts.map +1 -1
  83. package/dist/server/types.js +0 -4
  84. package/dist/server/types.js.map +1 -1
  85. package/dist/types.d.ts +2 -266
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/types.js +0 -6
  88. package/dist/types.js.map +1 -1
  89. package/package.json +5 -5
  90. package/src/styles/notifications.css +0 -14
  91. package/dist/react/components/index.d.ts +0 -5
  92. package/dist/react/components/index.d.ts.map +0 -1
  93. package/dist/react/components/index.js +0 -5
  94. package/dist/react/components/index.js.map +0 -1
  95. package/dist/react/hooks/index.d.ts +0 -6
  96. package/dist/react/hooks/index.d.ts.map +0 -1
  97. package/dist/react/hooks/index.js +0 -6
  98. package/dist/react/hooks/index.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/server/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;GAKG;AACH,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IACpD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;QAC1C,yEAAyE;QACzE,uDAAuD;QACvD,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,2BAA2B,CAAC,SAAS,CAAC,CAAC;IACrE,CAAC;CACF","sourcesContent":["/**\n * Typed errors thrown by notification-core server helpers.\n */\n\n/**\n * Thrown when a caller is not allowed to perform a notification action — e.g.\n * archiving a personal notification they do not own, or a non-admin archiving a\n * shared notification. Consuming callables should catch this and map it to a\n * `permission-denied` error code rather than surfacing a generic `internal`.\n */\nexport class NotificationPermissionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NotificationPermissionError';\n // Restore the prototype chain so `instanceof` works across the transpile\n // target (extending built-ins can break it otherwise).\n Object.setPrototypeOf(this, NotificationPermissionError.prototype);\n }\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/server/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IACpD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;QAC1C,iGAAiG;QACjG,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,2BAA2B,CAAC,SAAS,CAAC,CAAC;IACrE,CAAC;CACF","sourcesContent":["export class NotificationPermissionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NotificationPermissionError';\n // Restores the prototype chain, which extending a built-in can break under the transpile target.\n Object.setPrototypeOf(this, NotificationPermissionError.prototype);\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAGzE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EACb,aAAa,GACd,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,kCAAkC,EAClC,iCAAiC,GAClC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAGhF,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAEzE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EACb,aAAa,GACd,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,kCAAkC,EAClC,iCAAiC,GAClC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAEhF,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
@@ -1,9 +1,6 @@
1
- // Server-side helpers (for Cloud Functions)
2
1
  export { processBatchHelper } from './processBatchHelper.js';
3
2
  export { NotificationPermissionError } from './errors.js';
4
3
  export { buildActiveNotificationDocId } from './activeNotificationId.js';
5
- // Delivery ledger
6
4
  export { createDeliveryLedger, applyAggregation } from './delivery-ledger.js';
7
- // Observed-generation seen/archive protocol
8
5
  export { markNotificationSeenWithGeneration, archiveNotificationWithGeneration, } from './observed-generation.js';
9
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAEzE,kBAAkB;AAClB,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAkB9E,4CAA4C;AAC5C,OAAO,EACL,kCAAkC,EAClC,iCAAiC,GAClC,MAAM,0BAA0B,CAAC","sourcesContent":["// Server-side helpers (for Cloud Functions)\nexport { processBatchHelper } from './processBatchHelper.js';\nexport { NotificationPermissionError } from './errors.js';\nexport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\n// Delivery ledger\nexport { createDeliveryLedger, applyAggregation } from './delivery-ledger.js';\nexport type {\n DeliveryLedger,\n DeliveryRowInput,\n DeliveryPayload,\n DeliveryState,\n DeliverySkipReason,\n DeliveryLedgerOptions,\n AggregationStrategy,\n MaterializationClass,\n EnqueueResult,\n EnqueueRowResult,\n MaterializeOutcome,\n MaterializeManyOptions,\n ReplayOutcome,\n ReplayOptions,\n} from './delivery-ledger.js';\n\n// Observed-generation seen/archive protocol\nexport {\n markNotificationSeenWithGeneration,\n archiveNotificationWithGeneration,\n} from './observed-generation.js';\nexport type { MarkSeenOutcome, ArchiveOutcome } from './observed-generation.js';\n\n// Server types\nexport type {\n ServerFirestore,\n ServerCollectionRef,\n ServerQuery,\n ServerQuerySnapshot,\n ServerDocSnapshot,\n ServerDocRef,\n ServerWriteBatch,\n ServerTransaction,\n} from './types.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAEzE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAkB9E,OAAO,EACL,kCAAkC,EAClC,iCAAiC,GAClC,MAAM,0BAA0B,CAAC","sourcesContent":["export { processBatchHelper } from './processBatchHelper.js';\nexport { NotificationPermissionError } from './errors.js';\nexport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\nexport { createDeliveryLedger, applyAggregation } from './delivery-ledger.js';\nexport type {\n DeliveryLedger,\n DeliveryRowInput,\n DeliveryPayload,\n DeliveryState,\n DeliverySkipReason,\n DeliveryLedgerOptions,\n AggregationStrategy,\n MaterializationClass,\n EnqueueResult,\n EnqueueRowResult,\n MaterializeOutcome,\n MaterializeManyOptions,\n ReplayOutcome,\n ReplayOptions,\n} from './delivery-ledger.js';\n\nexport {\n markNotificationSeenWithGeneration,\n archiveNotificationWithGeneration,\n} from './observed-generation.js';\nexport type { MarkSeenOutcome, ArchiveOutcome } from './observed-generation.js';\n\nexport type {\n ServerFirestore,\n ServerCollectionRef,\n ServerQuery,\n ServerQuerySnapshot,\n ServerDocSnapshot,\n ServerDocRef,\n ServerWriteBatch,\n ServerTransaction,\n} from './types.js';\n"]}
@@ -1,62 +1,22 @@
1
- /**
2
- * Observed-generation seen/archive protocol (notification redesign — P1).
3
- *
4
- * The SEEN/ARCHIVE precondition compares the card's current opaque
5
- * `activityGeneration` against the generation the client actually observed when
6
- * it rendered the row — so activity arriving after render (or a delete+recreate
7
- * that rotated the token) is never silently swallowed. Archive additionally
8
- * carries a retry-stable `requestId`; the deterministic history doc id makes a
9
- * replay return the stored result and touch nothing.
10
- *
11
- * GENERIC: the app constructs the deterministic ids (`historyRef`, `payloadHash`)
12
- * via its own canonical hash (in the consuming app's core package) and passes the
13
- * refs in; this module owns only the transactional state machine, not the id formulas.
14
- */
15
1
  import type { ServerFirestore, ServerDocRef, ServerTransaction } from './types.js';
16
2
  export type MarkSeenOutcome = 'seen' | 'generation-mismatch' | 'missing';
17
- /**
18
- * Set `seenAt`/`seenAtGeneration` ONLY if the card's current `activityGeneration`
19
- * still matches the observed one; otherwise no-op (the row stays unseen so newer
20
- * activity is never swallowed).
21
- */
22
3
  export declare function markNotificationSeenWithGeneration(db: ServerFirestore, activeRef: ServerDocRef, params: {
23
4
  observedActivityGeneration: string;
24
5
  now?: number;
25
6
  }): Promise<MarkSeenOutcome>;
26
7
  export type ArchiveOutcome = 'archived' | 'replayed' | 'conflict' | 'generation-mismatch' | 'missing';
27
- /**
28
- * Archive one active card into history, idempotently and replay-safely.
29
- *
30
- * - history doc already exists + same `payloadHash` ⇒ `replayed` (touch NOTHING).
31
- * - history doc already exists + DIFFERENT `payloadHash` ⇒ `conflict` (caller alerts).
32
- * - first-seen: archive only if the active card's `activityGeneration` still
33
- * matches the observed generation; else `generation-mismatch` (leave it active).
34
- */
35
8
  export declare function archiveNotificationWithGeneration(db: ServerFirestore, params: {
36
9
  activeRef: ServerDocRef;
37
- /** Deterministic history doc id = hash('notification-archive', category, audienceScope, requestId) (app-built). */
38
10
  historyRef: ServerDocRef;
39
11
  requestId: string;
40
12
  observedActivityGeneration: string;
41
- /** Deterministic payload hash (app-built) — the replay-vs-conflict discriminator. */
42
13
  payloadHash: string;
43
14
  category: string;
44
15
  audienceScope: string;
45
- /** Native-TTL expiry for the history doc; converted to a Timestamp via the factory. */
46
16
  expireAtMs: number;
47
17
  timestampFromMillis: (ms: number) => unknown;
48
- /** Admin history quick-access field (admin category only). */
49
18
  handledBy?: string;
50
- /**
51
- * Optional hook to compose additional writes (e.g. an audit event) INTO the
52
- * same archive transaction, so they commit atomically with the active→history
53
- * move. Invoked in the write phase ONLY on the successful 'archived' path —
54
- * never on replay/conflict/generation-mismatch/missing — so the audit is
55
- * written iff the archive actually happens. Honor reads-before-writes: do not
56
- * read inside the hook. The hook is awaited inside the transaction body, so a
57
- * throw or a rejected promise fails the archive instead of committing it
58
- * without its write.
59
- */
19
+ /** Runs in the write phase, so it must not read (Firestore reads-before-writes). */
60
20
  auditWrite?: (txn: ServerTransaction) => void | Promise<void>;
61
21
  }): Promise<ArchiveOutcome>;
62
22
  //# sourceMappingURL=observed-generation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"observed-generation.d.ts","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAEzE;;;;GAIG;AACH,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,eAAe,EACnB,SAAS,EAAE,YAAY,EACvB,MAAM,EAAE;IAAE,0BAA0B,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3D,OAAO,CAAC,eAAe,CAAC,CAc1B;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAEtG;;;;;;;GAOG;AACH,wBAAsB,iCAAiC,CACrD,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE;IACN,SAAS,EAAE,YAAY,CAAC;IACxB,mHAAmH;IACnH,UAAU,EAAE,YAAY,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,0BAA0B,EAAE,MAAM,CAAC;IACnC,qFAAqF;IACrF,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D,GACA,OAAO,CAAC,cAAc,CAAC,CAoDzB"}
1
+ {"version":3,"file":"observed-generation.d.ts","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAEzE,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,eAAe,EACnB,SAAS,EAAE,YAAY,EACvB,MAAM,EAAE;IAAE,0BAA0B,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3D,OAAO,CAAC,eAAe,CAAC,CAc1B;AAED,MAAM,MAAM,cAAc,GACxB,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAE3E,wBAAsB,iCAAiC,CACrD,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE;IACN,SAAS,EAAE,YAAY,CAAC;IACxB,UAAU,EAAE,YAAY,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,0BAA0B,EAAE,MAAM,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D,GACA,OAAO,CAAC,cAAc,CAAC,CAiDzB"}
@@ -1,22 +1,3 @@
1
- /**
2
- * Observed-generation seen/archive protocol (notification redesign — P1).
3
- *
4
- * The SEEN/ARCHIVE precondition compares the card's current opaque
5
- * `activityGeneration` against the generation the client actually observed when
6
- * it rendered the row — so activity arriving after render (or a delete+recreate
7
- * that rotated the token) is never silently swallowed. Archive additionally
8
- * carries a retry-stable `requestId`; the deterministic history doc id makes a
9
- * replay return the stored result and touch nothing.
10
- *
11
- * GENERIC: the app constructs the deterministic ids (`historyRef`, `payloadHash`)
12
- * via its own canonical hash (in the consuming app's core package) and passes the
13
- * refs in; this module owns only the transactional state machine, not the id formulas.
14
- */
15
- /**
16
- * Set `seenAt`/`seenAtGeneration` ONLY if the card's current `activityGeneration`
17
- * still matches the observed one; otherwise no-op (the row stays unseen so newer
18
- * activity is never swallowed).
19
- */
20
1
  export async function markNotificationSeenWithGeneration(db, activeRef, params) {
21
2
  return db.runTransaction(async (tx) => {
22
3
  const snap = await tx.get(activeRef);
@@ -33,18 +14,10 @@ export async function markNotificationSeenWithGeneration(db, activeRef, params)
33
14
  return 'seen';
34
15
  });
35
16
  }
36
- /**
37
- * Archive one active card into history, idempotently and replay-safely.
38
- *
39
- * - history doc already exists + same `payloadHash` ⇒ `replayed` (touch NOTHING).
40
- * - history doc already exists + DIFFERENT `payloadHash` ⇒ `conflict` (caller alerts).
41
- * - first-seen: archive only if the active card's `activityGeneration` still
42
- * matches the observed generation; else `generation-mismatch` (leave it active).
43
- */
44
17
  export async function archiveNotificationWithGeneration(db, params) {
45
18
  const { activeRef, historyRef, requestId, observedActivityGeneration, payloadHash, category, audienceScope, expireAtMs, timestampFromMillis, handledBy, auditWrite, } = params;
46
19
  return db.runTransaction(async (tx) => {
47
- // Reads first (Firestore reads-before-writes).
20
+ // Firestore transactions take every read before any write.
48
21
  const historySnap = await tx.get(historyRef);
49
22
  if (historySnap.exists) {
50
23
  const existing = (historySnap.data() ?? {});
@@ -74,9 +47,6 @@ export async function archiveNotificationWithGeneration(db, params) {
74
47
  historyDoc.handledBy = handledBy;
75
48
  tx.set(historyRef, historyDoc);
76
49
  tx.delete(activeRef);
77
- // Compose any caller-supplied write (e.g. an audit event) into THIS
78
- // transaction so it commits atomically with the archive. Only reached on the
79
- // successful path, after all reads, so reads-before-writes is preserved.
80
50
  await auditWrite?.(tx);
81
51
  return 'archived';
82
52
  });
@@ -1 +1 @@
1
- {"version":3,"file":"observed-generation.js","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,EAAmB,EACnB,SAAuB,EACvB,MAA4D;IAE5D,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACnC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QAC5D,IAAK,IAAI,CAAC,kBAAyC,KAAK,MAAM,CAAC,0BAA0B,EAAE,CAAC;YAC1F,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QACD,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE;YACnB,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;YAChC,gBAAgB,EAAE,MAAM,CAAC,0BAA0B;SACpD,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC;AAID;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,EAAmB,EACnB,MA0BC;IAED,MAAM,EACJ,SAAS,EACT,UAAU,EACV,SAAS,EACT,0BAA0B,EAC1B,WAAW,EACX,QAAQ,EACR,aAAa,EACb,UAAU,EACV,mBAAmB,EACnB,SAAS,EACT,UAAU,GACX,GAAG,MAAM,CAAC;IAEX,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,+CAA+C;QAC/C,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;YACvE,OAAQ,QAAQ,CAAC,WAAkC,KAAK,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;QAChG,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACzC,MAAM,UAAU,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QACxE,IAAK,UAAU,CAAC,kBAAyC,KAAK,0BAA0B,EAAE,CAAC;YACzF,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,UAAU,GAA4B;YAC1C,mBAAmB,EAAE,UAAU,CAAC,EAAE;YAClC,SAAS;YACT,WAAW;YACX,QAAQ,EAAE,SAAS,CAAC,EAAE;YACtB,0BAA0B;YAC1B,QAAQ;YACR,aAAa;YACb,gBAAgB,EAAE,UAAU;YAC5B,UAAU,EAAE,GAAG;YACf,QAAQ,EAAE,mBAAmB,CAAC,UAAU,CAAC;SAC1C,CAAC;QACF,IAAI,SAAS,KAAK,SAAS;YAAE,UAAU,CAAC,SAAS,GAAG,SAAS,CAAC;QAE9D,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC/B,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACrB,oEAAoE;QACpE,6EAA6E;QAC7E,yEAAyE;QACzE,MAAM,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QACvB,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Observed-generation seen/archive protocol (notification redesign — P1).\n *\n * The SEEN/ARCHIVE precondition compares the card's current opaque\n * `activityGeneration` against the generation the client actually observed when\n * it rendered the row — so activity arriving after render (or a delete+recreate\n * that rotated the token) is never silently swallowed. Archive additionally\n * carries a retry-stable `requestId`; the deterministic history doc id makes a\n * replay return the stored result and touch nothing.\n *\n * GENERIC: the app constructs the deterministic ids (`historyRef`, `payloadHash`)\n * via its own canonical hash (in the consuming app's core package) and passes the\n * refs in; this module owns only the transactional state machine, not the id formulas.\n */\n\nimport type { ServerFirestore, ServerDocRef, ServerTransaction } from './types.js';\n\nexport type MarkSeenOutcome = 'seen' | 'generation-mismatch' | 'missing';\n\n/**\n * Set `seenAt`/`seenAtGeneration` ONLY if the card's current `activityGeneration`\n * still matches the observed one; otherwise no-op (the row stays unseen so newer\n * activity is never swallowed).\n */\nexport async function markNotificationSeenWithGeneration(\n db: ServerFirestore,\n activeRef: ServerDocRef,\n params: { observedActivityGeneration: string; now?: number },\n): Promise<MarkSeenOutcome> {\n return db.runTransaction(async (tx) => {\n const snap = await tx.get(activeRef);\n if (!snap.exists) return 'missing';\n const data = (snap.data() ?? {}) as Record<string, unknown>;\n if ((data.activityGeneration as string | undefined) !== params.observedActivityGeneration) {\n return 'generation-mismatch';\n }\n tx.update(activeRef, {\n seenAt: params.now ?? Date.now(),\n seenAtGeneration: params.observedActivityGeneration,\n });\n return 'seen';\n });\n}\n\nexport type ArchiveOutcome = 'archived' | 'replayed' | 'conflict' | 'generation-mismatch' | 'missing';\n\n/**\n * Archive one active card into history, idempotently and replay-safely.\n *\n * - history doc already exists + same `payloadHash` ⇒ `replayed` (touch NOTHING).\n * - history doc already exists + DIFFERENT `payloadHash` ⇒ `conflict` (caller alerts).\n * - first-seen: archive only if the active card's `activityGeneration` still\n * matches the observed generation; else `generation-mismatch` (leave it active).\n */\nexport async function archiveNotificationWithGeneration(\n db: ServerFirestore,\n params: {\n activeRef: ServerDocRef;\n /** Deterministic history doc id = hash('notification-archive', category, audienceScope, requestId) (app-built). */\n historyRef: ServerDocRef;\n requestId: string;\n observedActivityGeneration: string;\n /** Deterministic payload hash (app-built) — the replay-vs-conflict discriminator. */\n payloadHash: string;\n category: string;\n audienceScope: string;\n /** Native-TTL expiry for the history doc; converted to a Timestamp via the factory. */\n expireAtMs: number;\n timestampFromMillis: (ms: number) => unknown;\n /** Admin history quick-access field (admin category only). */\n handledBy?: string;\n /**\n * Optional hook to compose additional writes (e.g. an audit event) INTO the\n * same archive transaction, so they commit atomically with the active→history\n * move. Invoked in the write phase ONLY on the successful 'archived' path —\n * never on replay/conflict/generation-mismatch/missing — so the audit is\n * written iff the archive actually happens. Honor reads-before-writes: do not\n * read inside the hook. The hook is awaited inside the transaction body, so a\n * throw or a rejected promise fails the archive instead of committing it\n * without its write.\n */\n auditWrite?: (txn: ServerTransaction) => void | Promise<void>;\n },\n): Promise<ArchiveOutcome> {\n const {\n activeRef,\n historyRef,\n requestId,\n observedActivityGeneration,\n payloadHash,\n category,\n audienceScope,\n expireAtMs,\n timestampFromMillis,\n handledBy,\n auditWrite,\n } = params;\n\n return db.runTransaction(async (tx) => {\n // Reads first (Firestore reads-before-writes).\n const historySnap = await tx.get(historyRef);\n if (historySnap.exists) {\n const existing = (historySnap.data() ?? {}) as Record<string, unknown>;\n return (existing.payloadHash as string | undefined) === payloadHash ? 'replayed' : 'conflict';\n }\n const activeSnap = await tx.get(activeRef);\n if (!activeSnap.exists) return 'missing';\n const activeData = (activeSnap.data() ?? {}) as Record<string, unknown>;\n if ((activeData.activityGeneration as string | undefined) !== observedActivityGeneration) {\n return 'generation-mismatch';\n }\n\n const now = Date.now();\n const historyDoc: Record<string, unknown> = {\n archiveOccurrenceId: historyRef.id,\n requestId,\n payloadHash,\n activeId: activeRef.id,\n observedActivityGeneration,\n category,\n audienceScope,\n archivedSnapshot: activeData,\n archivedAt: now,\n expireAt: timestampFromMillis(expireAtMs),\n };\n if (handledBy !== undefined) historyDoc.handledBy = handledBy;\n\n tx.set(historyRef, historyDoc);\n tx.delete(activeRef);\n // Compose any caller-supplied write (e.g. an audit event) into THIS\n // transaction so it commits atomically with the archive. Only reached on the\n // successful path, after all reads, so reads-before-writes is preserved.\n await auditWrite?.(tx);\n return 'archived';\n });\n}\n"]}
1
+ {"version":3,"file":"observed-generation.js","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,EAAmB,EACnB,SAAuB,EACvB,MAA4D;IAE5D,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACnC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QAC5D,IAAK,IAAI,CAAC,kBAAyC,KAAK,MAAM,CAAC,0BAA0B,EAAE,CAAC;YAC1F,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QACD,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE;YACnB,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;YAChC,gBAAgB,EAAE,MAAM,CAAC,0BAA0B;SACpD,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC;AAKD,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,EAAmB,EACnB,MAaC;IAED,MAAM,EACJ,SAAS,EACT,UAAU,EACV,SAAS,EACT,0BAA0B,EAC1B,WAAW,EACX,QAAQ,EACR,aAAa,EACb,UAAU,EACV,mBAAmB,EACnB,SAAS,EACT,UAAU,GACX,GAAG,MAAM,CAAC;IAEX,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,2DAA2D;QAC3D,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;YACvE,OAAQ,QAAQ,CAAC,WAAkC,KAAK,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;QAChG,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACzC,MAAM,UAAU,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QACxE,IAAK,UAAU,CAAC,kBAAyC,KAAK,0BAA0B,EAAE,CAAC;YACzF,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,UAAU,GAA4B;YAC1C,mBAAmB,EAAE,UAAU,CAAC,EAAE;YAClC,SAAS;YACT,WAAW;YACX,QAAQ,EAAE,SAAS,CAAC,EAAE;YACtB,0BAA0B;YAC1B,QAAQ;YACR,aAAa;YACb,gBAAgB,EAAE,UAAU;YAC5B,UAAU,EAAE,GAAG;YACf,QAAQ,EAAE,mBAAmB,CAAC,UAAU,CAAC;SAC1C,CAAC;QACF,IAAI,SAAS,KAAK,SAAS;YAAE,UAAU,CAAC,SAAS,GAAG,SAAS,CAAC;QAE9D,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC/B,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACrB,MAAM,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QACvB,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ServerFirestore, ServerDocRef, ServerTransaction } from './types.js';\n\nexport type MarkSeenOutcome = 'seen' | 'generation-mismatch' | 'missing';\n\nexport async function markNotificationSeenWithGeneration(\n db: ServerFirestore,\n activeRef: ServerDocRef,\n params: { observedActivityGeneration: string; now?: number },\n): Promise<MarkSeenOutcome> {\n return db.runTransaction(async (tx) => {\n const snap = await tx.get(activeRef);\n if (!snap.exists) return 'missing';\n const data = (snap.data() ?? {}) as Record<string, unknown>;\n if ((data.activityGeneration as string | undefined) !== params.observedActivityGeneration) {\n return 'generation-mismatch';\n }\n tx.update(activeRef, {\n seenAt: params.now ?? Date.now(),\n seenAtGeneration: params.observedActivityGeneration,\n });\n return 'seen';\n });\n}\n\nexport type ArchiveOutcome =\n 'archived' | 'replayed' | 'conflict' | 'generation-mismatch' | 'missing';\n\nexport async function archiveNotificationWithGeneration(\n db: ServerFirestore,\n params: {\n activeRef: ServerDocRef;\n historyRef: ServerDocRef;\n requestId: string;\n observedActivityGeneration: string;\n payloadHash: string;\n category: string;\n audienceScope: string;\n expireAtMs: number;\n timestampFromMillis: (ms: number) => unknown;\n handledBy?: string;\n /** Runs in the write phase, so it must not read (Firestore reads-before-writes). */\n auditWrite?: (txn: ServerTransaction) => void | Promise<void>;\n },\n): Promise<ArchiveOutcome> {\n const {\n activeRef,\n historyRef,\n requestId,\n observedActivityGeneration,\n payloadHash,\n category,\n audienceScope,\n expireAtMs,\n timestampFromMillis,\n handledBy,\n auditWrite,\n } = params;\n\n return db.runTransaction(async (tx) => {\n // Firestore transactions take every read before any write.\n const historySnap = await tx.get(historyRef);\n if (historySnap.exists) {\n const existing = (historySnap.data() ?? {}) as Record<string, unknown>;\n return (existing.payloadHash as string | undefined) === payloadHash ? 'replayed' : 'conflict';\n }\n const activeSnap = await tx.get(activeRef);\n if (!activeSnap.exists) return 'missing';\n const activeData = (activeSnap.data() ?? {}) as Record<string, unknown>;\n if ((activeData.activityGeneration as string | undefined) !== observedActivityGeneration) {\n return 'generation-mismatch';\n }\n\n const now = Date.now();\n const historyDoc: Record<string, unknown> = {\n archiveOccurrenceId: historyRef.id,\n requestId,\n payloadHash,\n activeId: activeRef.id,\n observedActivityGeneration,\n category,\n audienceScope,\n archivedSnapshot: activeData,\n archivedAt: now,\n expireAt: timestampFromMillis(expireAtMs),\n };\n if (handledBy !== undefined) historyDoc.handledBy = handledBy;\n\n tx.set(historyRef, historyDoc);\n tx.delete(activeRef);\n await auditWrite?.(tx);\n return 'archived';\n });\n}\n"]}
@@ -1,7 +1,3 @@
1
- /**
2
- * Batch processor helper — called by the app's scheduled Cloud Function.
3
- * Reads pending notifications, groups by dedupKey, creates/increments active docs.
4
- */
5
1
  import type { NotificationSystemConfig } from '../types.js';
6
2
  import type { ServerFirestore } from './types.js';
7
3
  interface BatchProcessResult {
@@ -9,21 +5,6 @@ interface BatchProcessResult {
9
5
  notificationsCreated: number;
10
6
  notificationsUpdated: number;
11
7
  }
12
- /**
13
- * Process the pending notifications queue.
14
- *
15
- * @example
16
- * ```ts
17
- * // In your scheduled Cloud Function:
18
- * export const processNotificationBatch = onSchedule(
19
- * { schedule: 'every 10 minutes' },
20
- * async () => {
21
- * const result = await processBatchHelper(db as any, TTT_NOTIFICATION_CONFIG);
22
- * logger.info('Batch processed', result);
23
- * }
24
- * );
25
- * ```
26
- */
27
8
  export declare function processBatchHelper(db: ServerFirestore, config: NotificationSystemConfig): Promise<BatchProcessResult>;
28
9
  export {};
29
10
  //# sourceMappingURL=processBatchHelper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"processBatchHelper.d.ts","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAuB,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAQlD,UAAU,kBAAkB;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CAmM7B"}
1
+ {"version":3,"file":"processBatchHelper.d.ts","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAuB,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAQlD,UAAU,kBAAkB;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CAyK7B"}
@@ -1,30 +1,11 @@
1
- /**
2
- * Batch processor helper — called by the app's scheduled Cloud Function.
3
- * Reads pending notifications, groups by dedupKey, creates/increments active docs.
4
- */
5
1
  import { buildActiveNotificationDocId } from './activeNotificationId.js';
6
2
  const DEFAULT_COUNT_CAP = 5000;
7
3
  const DEFAULT_ACTOR_CAP = 5;
8
4
  const PROCESSING_BATCH_SIZE = 500;
9
5
  const MAX_ITERATIONS = 20;
10
- /**
11
- * Process the pending notifications queue.
12
- *
13
- * @example
14
- * ```ts
15
- * // In your scheduled Cloud Function:
16
- * export const processNotificationBatch = onSchedule(
17
- * { schedule: 'every 10 minutes' },
18
- * async () => {
19
- * const result = await processBatchHelper(db as any, TTT_NOTIFICATION_CONFIG);
20
- * logger.info('Batch processed', result);
21
- * }
22
- * );
23
- * ```
24
- */
25
6
  export async function processBatchHelper(db, config) {
26
7
  const pendingPath = config.pendingCollectionPath ?? 'pendingNotifications';
27
- const cutoff = Date.now() - 30_000; // Process items older than 30 seconds
8
+ const cutoff = Date.now() - 30_000;
28
9
  let totalProcessed = 0;
29
10
  let notificationsCreated = 0;
30
11
  let notificationsUpdated = 0;
@@ -32,7 +13,8 @@ export async function processBatchHelper(db, config) {
32
13
  let hasMore = true;
33
14
  while (hasMore && iterations < MAX_ITERATIONS) {
34
15
  iterations++;
35
- const pendingQuery = db.collection(pendingPath)
16
+ const pendingQuery = db
17
+ .collection(pendingPath)
36
18
  .where('createdAt', '<=', cutoff)
37
19
  .orderBy('createdAt', 'asc')
38
20
  .limit(PROCESSING_BATCH_SIZE);
@@ -40,7 +22,6 @@ export async function processBatchHelper(db, config) {
40
22
  if (snapshot.empty) {
41
23
  break;
42
24
  }
43
- // Group by dedupKey
44
25
  const groups = new Map();
45
26
  for (const docSnap of snapshot.docs) {
46
27
  const data = docSnap.data();
@@ -51,17 +32,12 @@ export async function processBatchHelper(db, config) {
51
32
  if (!categoryConfig)
52
33
  continue;
53
34
  const dedupKey = typeConfig.dedupKeyPattern(data.metadata);
54
- // Personal notifications dedup per-recipient: a multi-recipient fan-out
55
- // (same type/metadata, different targetUserId) must never collapse into
56
- // one recipient's active doc. Shared notifications are intentionally a
57
- // single doc for all admins (targetUserId is null).
58
35
  const groupKey = categoryConfig.audienceType === 'personal'
59
36
  ? `${data.category}::${data.targetUserId}::${dedupKey}`
60
37
  : `${data.category}::${dedupKey}`;
61
38
  const existing = groups.get(groupKey);
62
39
  if (existing) {
63
40
  existing.count++;
64
- // Add actor id if not already present
65
41
  if (!existing.actorIds.includes(data.actorId)) {
66
42
  existing.actorIds.push(data.actorId);
67
43
  }
@@ -79,16 +55,6 @@ export async function processBatchHelper(db, config) {
79
55
  });
80
56
  }
81
57
  }
82
- // Process each group: create or increment the group's active doc.
83
- // Each group's groupKey uniquely determines a single active doc — the doc ID
84
- // is a deterministic hash of exactly the dedupKey + category (+ targetUserId
85
- // for personal) that compose the groupKey — so two distinct groups can never
86
- // resolve to the same active doc and there is no cross-group contention. The
87
- // work per group is independent I/O, so Promise.all turns N sequential
88
- // round-trips into one parallel wave, removing the timeout risk under high
89
- // dedup-group counts. WITHIN a dedup scope, the transactional read-modify-write
90
- // on the deterministic ID makes concurrent writers (a racing real-time send,
91
- // or an overlapping batch run) converge on one doc with correct counts.
92
58
  const groupResults = await Promise.all(Array.from(groups.values()).map(async (group) => {
93
59
  const typeConfig = config.types[group.type];
94
60
  if (!typeConfig)
@@ -100,9 +66,6 @@ export async function processBatchHelper(db, config) {
100
66
  const dedupKey = typeConfig.dedupKeyPattern(group.metadata);
101
67
  const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;
102
68
  const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;
103
- // Personal notifications are scoped to the recipient so different
104
- // recipients never share an active doc; shared notifications omit
105
- // targetUserId by design.
106
69
  const activeDocRef = db.collection(activePath).doc(buildActiveNotificationDocId({
107
70
  category: group.category,
108
71
  audienceType: categoryConfig.audienceType,
@@ -112,30 +75,23 @@ export async function processBatchHelper(db, config) {
112
75
  return db.runTransaction(async (tx) => {
113
76
  const existingSnap = await tx.get(activeDocRef);
114
77
  if (existingSnap.exists) {
115
- // Increment existing
116
78
  const existingData = (existingSnap.data() ?? {});
117
79
  const currentCount = existingData.count || 1;
118
80
  const currentActorIds = existingData.latestActorIds || [];
119
- // Merge actor ids (new first, deduped, capped)
120
81
  const newActorIds = group.actorIds.filter((id) => !currentActorIds.includes(id));
121
- const mergedActorIds = [
122
- ...newActorIds,
123
- ...currentActorIds,
124
- ].slice(0, actorCap);
82
+ const mergedActorIds = [...newActorIds, ...currentActorIds].slice(0, actorCap);
125
83
  const newCount = Math.min(currentCount + group.count, countCap);
126
84
  tx.update(activeDocRef, {
127
85
  count: newCount,
128
86
  latestActorIds: mergedActorIds,
129
87
  message: typeConfig.messagePattern(group.metadata, newCount),
130
- // New activity on an existing notification re-lights the unread badge.
131
88
  seenAt: 0,
132
89
  updatedAt: Date.now(),
133
90
  });
134
91
  return 'updated';
135
92
  }
136
93
  else {
137
- // Create new notification. A type with no defaultTargetPath is
138
- // linkless — OMIT the field (Firestore rejects `undefined` values).
94
+ // Firestore rejects `undefined` values, so a linkless type omits the field.
139
95
  const targetPath = typeConfig.defaultTargetPath === undefined
140
96
  ? undefined
141
97
  : typeof typeConfig.defaultTargetPath === 'function'
@@ -153,7 +109,6 @@ export async function processBatchHelper(db, config) {
153
109
  latestActorIds: group.actorIds.slice(0, actorCap),
154
110
  ...(targetPath === undefined ? {} : { targetPath }),
155
111
  metadata: group.metadata,
156
- // Active docs are created unseen so the unread count() predicate matches.
157
112
  seenAt: 0,
158
113
  createdAt: now,
159
114
  updatedAt: now,
@@ -169,7 +124,6 @@ export async function processBatchHelper(db, config) {
169
124
  else if (r === 'updated')
170
125
  notificationsUpdated++;
171
126
  }
172
- // Delete processed pending docs in batches of 500
173
127
  const allDocs = snapshot.docs;
174
128
  for (let i = 0; i < allDocs.length; i += 500) {
175
129
  const chunk = allDocs.slice(i, i + 500);
@@ -1 +1 @@
1
- {"version":3,"file":"processBatchHelper.js","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAEzE,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAC/B,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,cAAc,GAAG,EAAE,CAAC;AAQ1B;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,EAAmB,EACnB,MAAgC;IAEhC,MAAM,WAAW,GAAG,MAAM,CAAC,qBAAqB,IAAI,sBAAsB,CAAC;IAC3E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,sCAAsC;IAE1E,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,IAAI,oBAAoB,GAAG,CAAC,CAAC;IAC7B,IAAI,oBAAoB,GAAG,CAAC,CAAC;IAC7B,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,OAAO,GAAG,IAAI,CAAC;IAEnB,OAAO,OAAO,IAAI,UAAU,GAAG,cAAc,EAAE,CAAC;QAC9C,UAAU,EAAE,CAAC;QAEb,MAAM,YAAY,GAAG,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC;aAC5C,KAAK,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC;aAChC,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC;aAC3B,KAAK,CAAC,qBAAqB,CAAC,CAAC;QAEhC,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,CAAC;QAE1C,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM;QACR,CAAC;QAED,oBAAoB;QACpB,MAAM,MAAM,GAAG,IAAI,GAAG,EAQlB,CAAC;QAEL,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAqC,CAAC;YAC/D,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,CAAC,UAAU;gBAAE,SAAS;YAC1B,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACxD,IAAI,CAAC,cAAc;gBAAE,SAAS;YAE9B,MAAM,QAAQ,GAAG,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC3D,wEAAwE;YACxE,wEAAwE;YACxE,uEAAuE;YACvE,oDAAoD;YACpD,MAAM,QAAQ,GAAG,cAAc,CAAC,YAAY,KAAK,UAAU;gBACzD,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY,KAAK,QAAQ,EAAE;gBACvD,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAEpC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACtC,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,KAAK,EAAE,CAAC;gBACjB,sCAAsC;gBACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC9C,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvC,CAAC;gBACD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC9B,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE;oBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,YAAY,EAAE,IAAI,CAAC,YAAY;oBAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,QAAQ,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;oBACxB,KAAK,EAAE,CAAC;oBACR,IAAI,EAAE,CAAC,OAAO,CAAC;iBAChB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,kEAAkE;QAClE,6EAA6E;QAC7E,6EAA6E;QAC7E,6EAA6E;QAC7E,6EAA6E;QAC7E,uEAAuE;QACvE,2EAA2E;QAC3E,gFAAgF;QAChF,6EAA6E;QAC7E,wEAAwE;QACxE,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,GAAG,CACpC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAC7B,KAAK,EAAE,KAAK,EAA8C,EAAE;YAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5C,IAAI,CAAC,UAAU;gBAAE,OAAO,SAAS,CAAC;YAElC,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACzD,IAAI,CAAC,cAAc;gBAAE,OAAO,SAAS,CAAC;YAEtC,MAAM,UAAU,GAAG,cAAc,CAAC,UAAU,CAAC;YAC7C,MAAM,QAAQ,GAAG,UAAU,CAAC,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC5D,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,IAAI,iBAAiB,CAAC;YAC1D,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,IAAI,iBAAiB,CAAC;YAE1D,kEAAkE;YAClE,kEAAkE;YAClE,0BAA0B;YAC1B,MAAM,YAAY,GAAG,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,GAAG,CAChD,4BAA4B,CAAC;gBAC3B,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,YAAY,EAAE,cAAc,CAAC,YAAY;gBACzC,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,QAAQ;aACT,CAAC,CACH,CAAC;YAEF,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAkC,EAAE;gBACpE,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBAEhD,IAAI,YAAY,CAAC,MAAM,EAAE,CAAC;oBACxB,qBAAqB;oBACrB,MAAM,YAAY,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;oBAC5E,MAAM,YAAY,GAAI,YAAY,CAAC,KAAgB,IAAI,CAAC,CAAC;oBACzD,MAAM,eAAe,GAAI,YAAY,CAAC,cAA2B,IAAI,EAAE,CAAC;oBAExE,+CAA+C;oBAC/C,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CACvC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC,CACtC,CAAC;oBACF,MAAM,cAAc,GAAG;wBACrB,GAAG,WAAW;wBACd,GAAG,eAAe;qBACnB,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;oBAErB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,GAAG,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;oBAEhE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;wBACtB,KAAK,EAAE,QAAQ;wBACf,cAAc,EAAE,cAAc;wBAC9B,OAAO,EAAE,UAAU,CAAC,cAAc,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC;wBAC5D,uEAAuE;wBACvE,MAAM,EAAE,CAAC;wBACT,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;qBACtB,CAAC,CAAC;oBAEH,OAAO,SAAS,CAAC;gBACnB,CAAC;qBAAM,CAAC;oBACN,+DAA+D;oBAC/D,oEAAoE;oBACpE,MAAM,UAAU,GAAG,UAAU,CAAC,iBAAiB,KAAK,SAAS;wBAC3D,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,OAAO,UAAU,CAAC,iBAAiB,KAAK,UAAU;4BAClD,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC;4BAC9C,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC;oBACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;oBAEvB,MAAM,MAAM,GAAG;wBACb,IAAI,EAAE,KAAK,CAAC,IAAI;wBAChB,QAAQ;wBACR,QAAQ,EAAE,KAAK,CAAC,QAAQ;wBACxB,YAAY,EAAE,KAAK,CAAC,YAAY;wBAChC,KAAK,EAAE,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC;wBAC9C,OAAO,EAAE,UAAU,CAAC,cAAc,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC;wBAC/D,KAAK,EAAE,KAAK,CAAC,KAAK;wBAClB,cAAc,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC;wBACjD,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC;wBACnD,QAAQ,EAAE,KAAK,CAAC,QAAQ;wBACxB,0EAA0E;wBAC1E,MAAM,EAAE,CAAC;wBACT,SAAS,EAAE,GAAG;wBACd,SAAS,EAAE,GAAG;qBACf,CAAC;oBAEF,EAAE,CAAC,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;oBAC7B,OAAO,SAAS,CAAC;gBACnB,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CACF,CACF,CAAC;QAEF,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;YAC7B,IAAI,CAAC,KAAK,SAAS;gBAAE,oBAAoB,EAAE,CAAC;iBACvC,IAAI,CAAC,KAAK,SAAS;gBAAE,oBAAoB,EAAE,CAAC;QACnD,CAAC;QAED,kDAAkD;QAClD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC;QAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC;YAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC;YACxC,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC;YACzB,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YACtD,MAAM,KAAK,CAAC,MAAM,EAAE,CAAC;QACvB,CAAC;QAED,cAAc,IAAI,QAAQ,CAAC,IAAI,CAAC;QAEhC,IAAI,QAAQ,CAAC,IAAI,GAAG,qBAAqB,EAAE,CAAC;YAC1C,OAAO,GAAG,KAAK,CAAC;QAClB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,CAAC;AACxE,CAAC","sourcesContent":["/**\n * Batch processor helper — called by the app's scheduled Cloud Function.\n * Reads pending notifications, groups by dedupKey, creates/increments active docs.\n */\n\nimport type { NotificationSystemConfig, PendingNotification } from '../types.js';\nimport type { ServerFirestore } from './types.js';\nimport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\nconst DEFAULT_COUNT_CAP = 5000;\nconst DEFAULT_ACTOR_CAP = 5;\nconst PROCESSING_BATCH_SIZE = 500;\nconst MAX_ITERATIONS = 20;\n\ninterface BatchProcessResult {\n totalProcessed: number;\n notificationsCreated: number;\n notificationsUpdated: number;\n}\n\n/**\n * Process the pending notifications queue.\n *\n * @example\n * ```ts\n * // In your scheduled Cloud Function:\n * export const processNotificationBatch = onSchedule(\n * { schedule: 'every 10 minutes' },\n * async () => {\n * const result = await processBatchHelper(db as any, TTT_NOTIFICATION_CONFIG);\n * logger.info('Batch processed', result);\n * }\n * );\n * ```\n */\nexport async function processBatchHelper(\n db: ServerFirestore,\n config: NotificationSystemConfig,\n): Promise<BatchProcessResult> {\n const pendingPath = config.pendingCollectionPath ?? 'pendingNotifications';\n const cutoff = Date.now() - 30_000; // Process items older than 30 seconds\n\n let totalProcessed = 0;\n let notificationsCreated = 0;\n let notificationsUpdated = 0;\n let iterations = 0;\n let hasMore = true;\n\n while (hasMore && iterations < MAX_ITERATIONS) {\n iterations++;\n\n const pendingQuery = db.collection(pendingPath)\n .where('createdAt', '<=', cutoff)\n .orderBy('createdAt', 'asc')\n .limit(PROCESSING_BATCH_SIZE);\n\n const snapshot = await pendingQuery.get();\n\n if (snapshot.empty) {\n break;\n }\n\n // Group by dedupKey\n const groups = new Map<string, {\n type: string;\n category: string;\n targetUserId: string | null;\n metadata: Record<string, unknown>;\n actorIds: string[];\n count: number;\n docs: typeof snapshot.docs;\n }>();\n\n for (const docSnap of snapshot.docs) {\n const data = docSnap.data() as Omit<PendingNotification, 'id'>;\n const typeConfig = config.types[data.type];\n if (!typeConfig) continue;\n const categoryConfig = config.categories[data.category];\n if (!categoryConfig) continue;\n\n const dedupKey = typeConfig.dedupKeyPattern(data.metadata);\n // Personal notifications dedup per-recipient: a multi-recipient fan-out\n // (same type/metadata, different targetUserId) must never collapse into\n // one recipient's active doc. Shared notifications are intentionally a\n // single doc for all admins (targetUserId is null).\n const groupKey = categoryConfig.audienceType === 'personal'\n ? `${data.category}::${data.targetUserId}::${dedupKey}`\n : `${data.category}::${dedupKey}`;\n\n const existing = groups.get(groupKey);\n if (existing) {\n existing.count++;\n // Add actor id if not already present\n if (!existing.actorIds.includes(data.actorId)) {\n existing.actorIds.push(data.actorId);\n }\n existing.docs.push(docSnap);\n } else {\n groups.set(groupKey, {\n type: data.type,\n category: data.category,\n targetUserId: data.targetUserId,\n metadata: data.metadata,\n actorIds: [data.actorId],\n count: 1,\n docs: [docSnap],\n });\n }\n }\n\n // Process each group: create or increment the group's active doc.\n // Each group's groupKey uniquely determines a single active doc — the doc ID\n // is a deterministic hash of exactly the dedupKey + category (+ targetUserId\n // for personal) that compose the groupKey — so two distinct groups can never\n // resolve to the same active doc and there is no cross-group contention. The\n // work per group is independent I/O, so Promise.all turns N sequential\n // round-trips into one parallel wave, removing the timeout risk under high\n // dedup-group counts. WITHIN a dedup scope, the transactional read-modify-write\n // on the deterministic ID makes concurrent writers (a racing real-time send,\n // or an overlapping batch run) converge on one doc with correct counts.\n const groupResults = await Promise.all(\n Array.from(groups.values()).map(\n async (group): Promise<'created' | 'updated' | 'skipped'> => {\n const typeConfig = config.types[group.type];\n if (!typeConfig) return 'skipped';\n\n const categoryConfig = config.categories[group.category];\n if (!categoryConfig) return 'skipped';\n\n const activePath = categoryConfig.activePath;\n const dedupKey = typeConfig.dedupKeyPattern(group.metadata);\n const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;\n const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;\n\n // Personal notifications are scoped to the recipient so different\n // recipients never share an active doc; shared notifications omit\n // targetUserId by design.\n const activeDocRef = db.collection(activePath).doc(\n buildActiveNotificationDocId({\n category: group.category,\n audienceType: categoryConfig.audienceType,\n targetUserId: group.targetUserId,\n dedupKey,\n }),\n );\n\n return db.runTransaction(async (tx): Promise<'created' | 'updated'> => {\n const existingSnap = await tx.get(activeDocRef);\n\n if (existingSnap.exists) {\n // Increment existing\n const existingData = (existingSnap.data() ?? {}) as Record<string, unknown>;\n const currentCount = (existingData.count as number) || 1;\n const currentActorIds = (existingData.latestActorIds as string[]) || [];\n\n // Merge actor ids (new first, deduped, capped)\n const newActorIds = group.actorIds.filter(\n (id) => !currentActorIds.includes(id)\n );\n const mergedActorIds = [\n ...newActorIds,\n ...currentActorIds,\n ].slice(0, actorCap);\n\n const newCount = Math.min(currentCount + group.count, countCap);\n\n tx.update(activeDocRef, {\n count: newCount,\n latestActorIds: mergedActorIds,\n message: typeConfig.messagePattern(group.metadata, newCount),\n // New activity on an existing notification re-lights the unread badge.\n seenAt: 0,\n updatedAt: Date.now(),\n });\n\n return 'updated';\n } else {\n // Create new notification. A type with no defaultTargetPath is\n // linkless — OMIT the field (Firestore rejects `undefined` values).\n const targetPath = typeConfig.defaultTargetPath === undefined\n ? undefined\n : typeof typeConfig.defaultTargetPath === 'function'\n ? typeConfig.defaultTargetPath(group.metadata)\n : typeConfig.defaultTargetPath;\n const now = Date.now();\n\n const newDoc = {\n type: group.type,\n dedupKey,\n category: group.category,\n targetUserId: group.targetUserId,\n title: typeConfig.titlePattern(group.metadata),\n message: typeConfig.messagePattern(group.metadata, group.count),\n count: group.count,\n latestActorIds: group.actorIds.slice(0, actorCap),\n ...(targetPath === undefined ? {} : { targetPath }),\n metadata: group.metadata,\n // Active docs are created unseen so the unread count() predicate matches.\n seenAt: 0,\n createdAt: now,\n updatedAt: now,\n };\n\n tx.set(activeDocRef, newDoc);\n return 'created';\n }\n });\n }\n )\n );\n\n for (const r of groupResults) {\n if (r === 'created') notificationsCreated++;\n else if (r === 'updated') notificationsUpdated++;\n }\n\n // Delete processed pending docs in batches of 500\n const allDocs = snapshot.docs;\n for (let i = 0; i < allDocs.length; i += 500) {\n const chunk = allDocs.slice(i, i + 500);\n const batch = db.batch();\n chunk.forEach((docSnap) => batch.delete(docSnap.ref));\n await batch.commit();\n }\n\n totalProcessed += snapshot.size;\n\n if (snapshot.size < PROCESSING_BATCH_SIZE) {\n hasMore = false;\n }\n }\n\n return { totalProcessed, notificationsCreated, notificationsUpdated };\n}\n"]}
1
+ {"version":3,"file":"processBatchHelper.js","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"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","sourcesContent":["import type { NotificationSystemConfig, PendingNotification } from '../types.js';\nimport type { ServerFirestore } from './types.js';\nimport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\nconst DEFAULT_COUNT_CAP = 5000;\nconst DEFAULT_ACTOR_CAP = 5;\nconst PROCESSING_BATCH_SIZE = 500;\nconst MAX_ITERATIONS = 20;\n\ninterface BatchProcessResult {\n totalProcessed: number;\n notificationsCreated: number;\n notificationsUpdated: number;\n}\n\nexport async function processBatchHelper(\n db: ServerFirestore,\n config: NotificationSystemConfig,\n): Promise<BatchProcessResult> {\n const pendingPath = config.pendingCollectionPath ?? 'pendingNotifications';\n const cutoff = Date.now() - 30_000;\n\n let totalProcessed = 0;\n let notificationsCreated = 0;\n let notificationsUpdated = 0;\n let iterations = 0;\n let hasMore = true;\n\n while (hasMore && iterations < MAX_ITERATIONS) {\n iterations++;\n\n const pendingQuery = db\n .collection(pendingPath)\n .where('createdAt', '<=', cutoff)\n .orderBy('createdAt', 'asc')\n .limit(PROCESSING_BATCH_SIZE);\n\n const snapshot = await pendingQuery.get();\n\n if (snapshot.empty) {\n break;\n }\n\n const groups = new Map<\n string,\n {\n type: string;\n category: string;\n targetUserId: string | null;\n metadata: Record<string, unknown>;\n actorIds: string[];\n count: number;\n docs: typeof snapshot.docs;\n }\n >();\n\n for (const docSnap of snapshot.docs) {\n const data = docSnap.data() as Omit<PendingNotification, 'id'>;\n const typeConfig = config.types[data.type];\n if (!typeConfig) continue;\n const categoryConfig = config.categories[data.category];\n if (!categoryConfig) continue;\n\n const dedupKey = typeConfig.dedupKeyPattern(data.metadata);\n const groupKey =\n categoryConfig.audienceType === 'personal'\n ? `${data.category}::${data.targetUserId}::${dedupKey}`\n : `${data.category}::${dedupKey}`;\n\n const existing = groups.get(groupKey);\n if (existing) {\n existing.count++;\n if (!existing.actorIds.includes(data.actorId)) {\n existing.actorIds.push(data.actorId);\n }\n existing.docs.push(docSnap);\n } else {\n groups.set(groupKey, {\n type: data.type,\n category: data.category,\n targetUserId: data.targetUserId,\n metadata: data.metadata,\n actorIds: [data.actorId],\n count: 1,\n docs: [docSnap],\n });\n }\n }\n\n const groupResults = await Promise.all(\n Array.from(groups.values()).map(async (group): Promise<'created' | 'updated' | 'skipped'> => {\n const typeConfig = config.types[group.type];\n if (!typeConfig) return 'skipped';\n\n const categoryConfig = config.categories[group.category];\n if (!categoryConfig) return 'skipped';\n\n const activePath = categoryConfig.activePath;\n const dedupKey = typeConfig.dedupKeyPattern(group.metadata);\n const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;\n const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;\n\n const activeDocRef = db.collection(activePath).doc(\n buildActiveNotificationDocId({\n category: group.category,\n audienceType: categoryConfig.audienceType,\n targetUserId: group.targetUserId,\n dedupKey,\n }),\n );\n\n return db.runTransaction(async (tx): Promise<'created' | 'updated'> => {\n const existingSnap = await tx.get(activeDocRef);\n\n if (existingSnap.exists) {\n const existingData = (existingSnap.data() ?? {}) as Record<string, unknown>;\n const currentCount = (existingData.count as number) || 1;\n const currentActorIds = (existingData.latestActorIds as string[]) || [];\n\n const newActorIds = group.actorIds.filter((id) => !currentActorIds.includes(id));\n const mergedActorIds = [...newActorIds, ...currentActorIds].slice(0, actorCap);\n\n const newCount = Math.min(currentCount + group.count, countCap);\n\n tx.update(activeDocRef, {\n count: newCount,\n latestActorIds: mergedActorIds,\n message: typeConfig.messagePattern(group.metadata, newCount),\n seenAt: 0,\n updatedAt: Date.now(),\n });\n\n return 'updated';\n } else {\n // Firestore rejects `undefined` values, so a linkless type omits the field.\n const targetPath =\n typeConfig.defaultTargetPath === undefined\n ? undefined\n : typeof typeConfig.defaultTargetPath === 'function'\n ? typeConfig.defaultTargetPath(group.metadata)\n : typeConfig.defaultTargetPath;\n const now = Date.now();\n\n const newDoc = {\n type: group.type,\n dedupKey,\n category: group.category,\n targetUserId: group.targetUserId,\n title: typeConfig.titlePattern(group.metadata),\n message: typeConfig.messagePattern(group.metadata, group.count),\n count: group.count,\n latestActorIds: group.actorIds.slice(0, actorCap),\n ...(targetPath === undefined ? {} : { targetPath }),\n metadata: group.metadata,\n seenAt: 0,\n createdAt: now,\n updatedAt: now,\n };\n\n tx.set(activeDocRef, newDoc);\n return 'created';\n }\n });\n }),\n );\n\n for (const r of groupResults) {\n if (r === 'created') notificationsCreated++;\n else if (r === 'updated') notificationsUpdated++;\n }\n\n const allDocs = snapshot.docs;\n for (let i = 0; i < allDocs.length; i += 500) {\n const chunk = allDocs.slice(i, i + 500);\n const batch = db.batch();\n chunk.forEach((docSnap) => batch.delete(docSnap.ref));\n await batch.commit();\n }\n\n totalProcessed += snapshot.size;\n\n if (snapshot.size < PROCESSING_BATCH_SIZE) {\n hasMore = false;\n }\n }\n\n return { totalProcessed, notificationsCreated, notificationsUpdated };\n}\n"]}
@@ -1,11 +1,3 @@
1
- /**
2
- * Server-side types for notification-core backend helpers.
3
- * These use firebase-admin Firestore types passed in by the calling app.
4
- */
5
- /**
6
- * Firestore interface — matches firebase-admin's Firestore type.
7
- * We accept this instead of importing firebase-admin directly.
8
- */
9
1
  export interface ServerFirestore {
10
2
  collection(path: string): ServerCollectionRef;
11
3
  doc(path: string): ServerDocRef;
@@ -50,7 +42,6 @@ export interface ServerDocRef {
50
42
  merge?: boolean;
51
43
  }): Promise<unknown>;
52
44
  update(data: Record<string, unknown>): Promise<unknown>;
53
- /** Create-if-absent. Rejects with an `already-exists` error (gRPC code 6) if the doc exists. */
54
45
  create(data: Record<string, unknown>): Promise<unknown>;
55
46
  delete(): Promise<unknown>;
56
47
  get(): Promise<ServerDocSnapshot>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,2 @@
1
- /**
2
- * Server-side types for notification-core backend helpers.
3
- * These use firebase-admin Firestore types passed in by the calling app.
4
- */
5
1
  export {};
6
2
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * Server-side types for notification-core backend helpers.\n * These use firebase-admin Firestore types passed in by the calling app.\n */\n\n/**\n * Firestore interface — matches firebase-admin's Firestore type.\n * We accept this instead of importing firebase-admin directly.\n */\nexport interface ServerFirestore {\n collection(path: string): ServerCollectionRef;\n doc(path: string): ServerDocRef;\n batch(): ServerWriteBatch;\n runTransaction<T>(updateFunction: (transaction: ServerTransaction) => Promise<T>): Promise<T>;\n}\n\nexport interface ServerTransaction {\n get(ref: ServerDocRef): Promise<ServerDocSnapshot>;\n set(ref: ServerDocRef, data: Record<string, unknown>, options?: { merge?: boolean }): ServerTransaction;\n update(ref: ServerDocRef, data: Record<string, unknown>): ServerTransaction;\n delete(ref: ServerDocRef): ServerTransaction;\n}\n\nexport interface ServerCollectionRef {\n doc(id?: string): ServerDocRef;\n where(field: string, op: string, value: unknown): ServerQuery;\n orderBy(field: string, direction?: 'asc' | 'desc'): ServerQuery;\n limit(n: number): ServerQuery;\n add(data: Record<string, unknown>): Promise<ServerDocRef>;\n}\n\nexport interface ServerQuery {\n where(field: string, op: string, value: unknown): ServerQuery;\n orderBy(field: string, direction?: 'asc' | 'desc'): ServerQuery;\n limit(n: number): ServerQuery;\n get(): Promise<ServerQuerySnapshot>;\n}\n\nexport interface ServerQuerySnapshot {\n empty: boolean;\n size: number;\n docs: ServerDocSnapshot[];\n}\n\nexport interface ServerDocSnapshot {\n id: string;\n exists: boolean;\n data(): Record<string, unknown> | undefined;\n ref: ServerDocRef;\n}\n\nexport interface ServerDocRef {\n id: string;\n set(data: Record<string, unknown>, options?: { merge?: boolean }): Promise<unknown>;\n update(data: Record<string, unknown>): Promise<unknown>;\n /** Create-if-absent. Rejects with an `already-exists` error (gRPC code 6) if the doc exists. */\n create(data: Record<string, unknown>): Promise<unknown>;\n delete(): Promise<unknown>;\n get(): Promise<ServerDocSnapshot>;\n}\n\nexport interface ServerWriteBatch {\n set(ref: ServerDocRef, data: Record<string, unknown>, options?: { merge?: boolean }): ServerWriteBatch;\n update(ref: ServerDocRef, data: Record<string, unknown>): ServerWriteBatch;\n delete(ref: ServerDocRef): ServerWriteBatch;\n commit(): Promise<unknown>;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface ServerFirestore {\n collection(path: string): ServerCollectionRef;\n doc(path: string): ServerDocRef;\n batch(): ServerWriteBatch;\n runTransaction<T>(updateFunction: (transaction: ServerTransaction) => Promise<T>): Promise<T>;\n}\n\nexport interface ServerTransaction {\n get(ref: ServerDocRef): Promise<ServerDocSnapshot>;\n set(\n ref: ServerDocRef,\n data: Record<string, unknown>,\n options?: { merge?: boolean },\n ): ServerTransaction;\n update(ref: ServerDocRef, data: Record<string, unknown>): ServerTransaction;\n delete(ref: ServerDocRef): ServerTransaction;\n}\n\nexport interface ServerCollectionRef {\n doc(id?: string): ServerDocRef;\n where(field: string, op: string, value: unknown): ServerQuery;\n orderBy(field: string, direction?: 'asc' | 'desc'): ServerQuery;\n limit(n: number): ServerQuery;\n add(data: Record<string, unknown>): Promise<ServerDocRef>;\n}\n\nexport interface ServerQuery {\n where(field: string, op: string, value: unknown): ServerQuery;\n orderBy(field: string, direction?: 'asc' | 'desc'): ServerQuery;\n limit(n: number): ServerQuery;\n get(): Promise<ServerQuerySnapshot>;\n}\n\nexport interface ServerQuerySnapshot {\n empty: boolean;\n size: number;\n docs: ServerDocSnapshot[];\n}\n\nexport interface ServerDocSnapshot {\n id: string;\n exists: boolean;\n data(): Record<string, unknown> | undefined;\n ref: ServerDocRef;\n}\n\nexport interface ServerDocRef {\n id: string;\n set(data: Record<string, unknown>, options?: { merge?: boolean }): Promise<unknown>;\n update(data: Record<string, unknown>): Promise<unknown>;\n create(data: Record<string, unknown>): Promise<unknown>;\n delete(): Promise<unknown>;\n get(): Promise<ServerDocSnapshot>;\n}\n\nexport interface ServerWriteBatch {\n set(\n ref: ServerDocRef,\n data: Record<string, unknown>,\n options?: { merge?: boolean },\n ): ServerWriteBatch;\n update(ref: ServerDocRef, data: Record<string, unknown>): ServerWriteBatch;\n delete(ref: ServerDocRef): ServerWriteBatch;\n commit(): Promise<unknown>;\n}\n"]}