@ttt-productions/notification-core 0.15.17 → 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 (87) hide show
  1. package/README.md +9 -10
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +0 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/react/components/NotificationEmptyState.d.ts +0 -3
  6. package/dist/react/components/NotificationEmptyState.d.ts.map +1 -1
  7. package/dist/react/components/NotificationEmptyState.js +0 -3
  8. package/dist/react/components/NotificationEmptyState.js.map +1 -1
  9. package/dist/react/components/NotificationHistoryList.d.ts +0 -6
  10. package/dist/react/components/NotificationHistoryList.d.ts.map +1 -1
  11. package/dist/react/components/NotificationHistoryList.js +0 -8
  12. package/dist/react/components/NotificationHistoryList.js.map +1 -1
  13. package/dist/react/components/NotificationList.d.ts +0 -3
  14. package/dist/react/components/NotificationList.d.ts.map +1 -1
  15. package/dist/react/components/NotificationList.js +2 -29
  16. package/dist/react/components/NotificationList.js.map +1 -1
  17. package/dist/react/components/NotificationUnreadBadge.d.ts +0 -5
  18. package/dist/react/components/NotificationUnreadBadge.d.ts.map +1 -1
  19. package/dist/react/components/NotificationUnreadBadge.js +0 -5
  20. package/dist/react/components/NotificationUnreadBadge.js.map +1 -1
  21. package/dist/react/components/relative-time.d.ts +0 -3
  22. package/dist/react/components/relative-time.d.ts.map +1 -1
  23. package/dist/react/components/relative-time.js +0 -3
  24. package/dist/react/components/relative-time.js.map +1 -1
  25. package/dist/react/hooks/useActiveNotifications.d.ts +0 -4
  26. package/dist/react/hooks/useActiveNotifications.d.ts.map +1 -1
  27. package/dist/react/hooks/useActiveNotifications.js +2 -6
  28. package/dist/react/hooks/useActiveNotifications.js.map +1 -1
  29. package/dist/react/hooks/useArchiveAllNotifications.d.ts +0 -30
  30. package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
  31. package/dist/react/hooks/useArchiveAllNotifications.js +1 -43
  32. package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
  33. package/dist/react/hooks/useArchiveNotification.d.ts +0 -8
  34. package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
  35. package/dist/react/hooks/useArchiveNotification.js +0 -8
  36. package/dist/react/hooks/useArchiveNotification.js.map +1 -1
  37. package/dist/react/hooks/useNotificationHistory.d.ts +0 -14
  38. package/dist/react/hooks/useNotificationHistory.d.ts.map +1 -1
  39. package/dist/react/hooks/useNotificationHistory.js +2 -18
  40. package/dist/react/hooks/useNotificationHistory.js.map +1 -1
  41. package/dist/react/hooks/useUnreadCount.d.ts +0 -22
  42. package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
  43. package/dist/react/hooks/useUnreadCount.js +1 -22
  44. package/dist/react/hooks/useUnreadCount.js.map +1 -1
  45. package/dist/react/index.d.ts.map +1 -1
  46. package/dist/react/index.js +0 -2
  47. package/dist/react/index.js.map +1 -1
  48. package/dist/react/types.d.ts +0 -42
  49. package/dist/react/types.d.ts.map +1 -1
  50. package/dist/react/types.js.map +1 -1
  51. package/dist/schemas.d.ts +0 -22
  52. package/dist/schemas.d.ts.map +1 -1
  53. package/dist/schemas.js +0 -25
  54. package/dist/schemas.js.map +1 -1
  55. package/dist/server/activeNotificationId.d.ts +1 -22
  56. package/dist/server/activeNotificationId.d.ts.map +1 -1
  57. package/dist/server/activeNotificationId.js +0 -16
  58. package/dist/server/activeNotificationId.js.map +1 -1
  59. package/dist/server/delivery-ledger.d.ts +1 -73
  60. package/dist/server/delivery-ledger.d.ts.map +1 -1
  61. package/dist/server/delivery-ledger.js +7 -59
  62. package/dist/server/delivery-ledger.js.map +1 -1
  63. package/dist/server/errors.d.ts +0 -9
  64. package/dist/server/errors.d.ts.map +1 -1
  65. package/dist/server/errors.js +1 -11
  66. package/dist/server/errors.js.map +1 -1
  67. package/dist/server/index.d.ts.map +1 -1
  68. package/dist/server/index.js +0 -3
  69. package/dist/server/index.js.map +1 -1
  70. package/dist/server/observed-generation.d.ts +1 -41
  71. package/dist/server/observed-generation.d.ts.map +1 -1
  72. package/dist/server/observed-generation.js +1 -31
  73. package/dist/server/observed-generation.js.map +1 -1
  74. package/dist/server/processBatchHelper.d.ts +0 -19
  75. package/dist/server/processBatchHelper.d.ts.map +1 -1
  76. package/dist/server/processBatchHelper.js +2 -46
  77. package/dist/server/processBatchHelper.js.map +1 -1
  78. package/dist/server/types.d.ts +0 -9
  79. package/dist/server/types.d.ts.map +1 -1
  80. package/dist/server/types.js +0 -4
  81. package/dist/server/types.js.map +1 -1
  82. package/dist/types.d.ts +2 -196
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js +0 -6
  85. package/dist/types.js.map +1 -1
  86. package/package.json +5 -5
  87. package/src/styles/notifications.css +0 -14
@@ -1 +1 @@
1
- {"version":3,"file":"activeNotificationId.js","sourceRoot":"","sources":["../../src/server/activeNotificationId.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAY5C;IACC,MAAM,KAAK,GACT,MAAM,CAAC,YAAY,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,MAAM,CAAC,YAAY,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC1F,MAAM,KAAK,GACT,MAAM,CAAC,gBAAgB,KAAK,SAAS;QACnC,CAAC,CAAC,gBAAgB,MAAM,CAAC,gBAAgB,IAAI,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE;QACvE,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,IAAI,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;IACvD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACvE,CAAC","sourcesContent":["/**\n * Deterministic active-notification document IDs.\n *\n * The active doc for a given dedup scope always lives at the same ID, so\n * concurrent writers (real-time sends racing each other or the batch\n * materializer) converge on one document inside a transaction instead of\n * racing a query-then-write. The ID hashes the same fields the dedup lookup\n * used to filter on: category, audience scope, and dedupKey.\n */\n\nimport { createHash } from 'node:crypto';\n\n/**\n * Build the deterministic doc ID for an active notification.\n *\n * Personal notifications scope per recipient (a fan-out to many users must\n * never collapse into one recipient's doc); shared notifications are\n * intentionally a single doc for the whole audience.\n */\nexport function buildActiveNotificationDocId(params: {\n category: string;\n audienceType: 'personal' | 'shared';\n targetUserId: string | null | undefined;\n dedupKey: string;\n /**\n * When provided, the id becomes **type-scoped** so two notification types that\n * share an aggregation key never collapse onto one active doc (the delivery\n * ledger materializer always passes it). Omitted => the legacy non-type-scoped\n * id, kept byte-identical for the pre-cutover composer path.\n */\n notificationType?: string;\n}): string {\n const scope =\n params.audienceType === 'personal' ? `user:${params.targetUserId ?? 'null'}` : 'shared';\n const input =\n params.notificationType !== undefined\n ? `notif-active ${params.notificationType} ${scope} ${params.dedupKey}`\n : `${params.category} ${scope} ${params.dedupKey}`;\n return createHash('sha256').update(input).digest('hex').slice(0, 40);\n}\n"]}
1
+ {"version":3,"file":"activeNotificationId.js","sourceRoot":"","sources":["../../src/server/activeNotificationId.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,UAAU,4BAA4B,CAAC,MAO5C;IACC,MAAM,KAAK,GACT,MAAM,CAAC,YAAY,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,MAAM,CAAC,YAAY,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC1F,MAAM,KAAK,GACT,MAAM,CAAC,gBAAgB,KAAK,SAAS;QACnC,CAAC,CAAC,gBAAgB,MAAM,CAAC,gBAAgB,IAAI,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE;QACvE,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,IAAI,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;IACvD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACvE,CAAC","sourcesContent":["import { createHash } from 'node:crypto';\n\nexport function buildActiveNotificationDocId(params: {\n category: string;\n audienceType: 'personal' | 'shared';\n targetUserId: string | null | undefined;\n dedupKey: string;\n /** Omit for processBatchHelper's id, which must stay byte-identical: existing active docs live at it. */\n notificationType?: string;\n}): string {\n const scope =\n params.audienceType === 'personal' ? `user:${params.targetUserId ?? 'null'}` : 'shared';\n const input =\n params.notificationType !== undefined\n ? `notif-active ${params.notificationType} ${scope} ${params.dedupKey}`\n : `${params.category} ${scope} ${params.dedupKey}`;\n return createHash('sha256').update(input).digest('hex').slice(0, 40);\n}\n"]}
@@ -1,68 +1,22 @@
1
- /**
2
- * Generic notification delivery ledger (notification redesign — P1).
3
- *
4
- * One Firestore doc per (recipient|shared, type, occurrence) is BOTH the queue
5
- * row AND the idempotency ledger. This package owns the GENERIC mechanism;
6
- * the consuming app owns the concrete collection name, the deterministic
7
- * `deliveryId`/`eventId`/`aggregationKey` construction, and the worker that
8
- * selects rows per `materializationClass`. The app passes fully-formed
9
- * `DeliveryRowInput`s; this module never invents domain ids.
10
- *
11
- * Key invariants (FROZEN — this module is their single source; the consuming app's
12
- * notification design doc explains the product rationale):
13
- * - Enqueue = create-if-absent. `ALREADY_EXISTS` is a per-row duplicate no-op,
14
- * never a failure — there is no marker-without-payload state, so the
15
- * "marker created, payload lost" crash window cannot exist.
16
- * - Materialize = ONE transaction that reads the delivery row + active card,
17
- * applies the aggregation exactly once (per strategy + caps), rotates the
18
- * opaque `activityGeneration`, resets `seenAt`, and flips the row to
19
- * `materialized` — so the "applied but not marked" double-apply window is gone.
20
- * Retrying a `materialized` row is a successful no-op.
21
- * - The same transaction asks the app whether the recipient may still receive
22
- * (`options.isRecipientEligible`); an ineligible recipient gets no card and the
23
- * row turns terminal `skipped`, so a row queued before (or retried after) the
24
- * recipient became ineligible can never recreate a card. Re-running it is a no-op.
25
- * - TTL (`expireAt`, a real Firestore Timestamp via `config.timestampFromMillis`)
26
- * is set ONLY at `materialized` / `skipped`; a `queued` or `deadLetter` row is NEVER TTL'd
27
- * so an unresolved delivery can't be silently deleted before replay.
28
- */
29
1
  import type { NotificationSystemConfig, NotificationDoc } from '../types.js';
30
2
  import type { ServerFirestore, ServerTransaction } from './types.js';
31
- /**
32
- * `skipped` is terminal: the recipient was not eligible when the row was materialized, so no card
33
- * was written. It is TTL'd like `materialized` and never retried.
34
- */
35
3
  export type DeliveryState = 'queued' | 'materialized' | 'skipped' | 'deadLetter';
36
- /** Why a row was terminalized as `skipped`. */
37
4
  export type DeliverySkipReason = 'recipientIneligible';
38
- /**
39
- * Ledger options the consuming app must decide.
40
- */
41
5
  export interface DeliveryLedgerOptions {
42
- /**
43
- * Whether `recipientUid` may still receive a notification. Called INSIDE the materialize
44
- * transaction, after the delivery row is read and before anything is written, so the answer
45
- * and the card write commit together (BACKEND-116): read through `tx` only. A recipient that
46
- * answers false gets no card, and the row becomes `skipped`. Not called for a shared
47
- * (recipient-less) row. A consumer with no eligibility rule answers true.
48
- */
6
+ /** Runs inside the materialize transaction before any write: read through `tx` only (RULE-STD-BACKEND-116). */
49
7
  isRecipientEligible: (recipientUid: string, tx: ServerTransaction) => Promise<boolean>;
50
8
  }
51
9
  export type AggregationStrategy = 'increment' | 'staticRelight';
52
10
  export type MaterializationClass = 'directQueued' | 'realtimeFallback' | 'fanoutOrphan' | 'retry';
53
- /** The materialization payload carried on every delivery row. */
54
11
  export interface DeliveryPayload {
55
- /** The acting user's uid, or `null`/`''` for a system / no-actor occurrence (N-M2). */
56
12
  actorId: string | null;
57
13
  metadata: Record<string, unknown>;
58
14
  occurrenceAt: number;
59
15
  }
60
- /** App-supplied, fully-formed delivery row (the app owns all id construction). */
61
16
  export interface DeliveryRowInput {
62
17
  deliveryId: string;
63
18
  notificationType: string;
64
19
  eventId: string;
65
- /** `null` for shared-admin occurrences (the deliveryId hash uses the literal `'shared'`). */
66
20
  recipientUid: string | null;
67
21
  aggregationKey: string;
68
22
  strategy: AggregationStrategy;
@@ -83,53 +37,27 @@ export type EnqueueRowResult = {
83
37
  };
84
38
  export interface EnqueueResult {
85
39
  results: EnqueueRowResult[];
86
- /** True iff EVERY row is now represented by a created-or-pre-existing doc (page-cursor invariant input). */
87
40
  allResolved: boolean;
88
41
  }
89
42
  export type MaterializeOutcome = 'materialized' | 'already-materialized' | 'skipped-ineligible-recipient' | 'missing' | 'skipped-non-queued';
90
43
  export interface MaterializeManyOptions {
91
- /** Rows materialized at once (default 10). */
92
44
  concurrency?: number;
93
- /**
94
- * Called for a row whose materialize threw and whose failure could not then be recorded either —
95
- * `error` is the recording's own failure and `details.cause` the materialize failure it was
96
- * recording. The row stays as it was (queued, attempt count
97
- * unchanged), so a later run selects it again; the rest of the batch proceeds. The app's capture
98
- * goes here; without it the ledger writes the failure to `console.error`.
99
- */
100
45
  onUnrecordedFailure?: (deliveryId: string, error: unknown, details: {
101
46
  cause: unknown;
102
47
  }) => void;
103
48
  }
104
- /** What a `replay` did: re-queued the row, found no row, or found a row that is not dead-lettered. */
105
49
  export type ReplayOutcome = 'replayed' | 'missing' | 'not-dead-lettered';
106
50
  export interface ReplayOptions {
107
- /**
108
- * The caller's own write (its audit event), composed into the replay transaction and awaited
109
- * there on the `replayed` path only. A hook that throws or rejects fails the replay, so the
110
- * reset never commits without it.
111
- */
112
51
  auditWrite?: (txn: ServerTransaction) => void | Promise<void>;
113
52
  }
114
53
  export interface DeliveryLedger {
115
54
  enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult>;
116
55
  materialize(deliveryId: string): Promise<MaterializeOutcome>;
117
- /**
118
- * Materialize many rows with bounded concurrency. A row whose materialize throws has the failure
119
- * recorded (a retry or a dead letter); each row is isolated, so one row's failure — even one
120
- * whose recording also fails — never fails the batch or stops the rows behind it.
121
- */
122
56
  materializeMany(deliveryIds: string[], options?: MaterializeManyOptions): Promise<Record<MaterializeOutcome, number>>;
123
57
  recordTransientFailure(deliveryId: string, error: unknown): Promise<void>;
124
58
  deadLetter(deliveryId: string, error: unknown): Promise<void>;
125
- /** Re-queue a dead-lettered row. A missing or non-dead-lettered row is answered, never written. */
126
59
  replay(deliveryId: string, options?: ReplayOptions): Promise<ReplayOutcome>;
127
60
  }
128
- /**
129
- * Apply the aggregation strategy to produce the active-doc field delta.
130
- * PURE — exported for direct unit testing. `existing` is the current active doc
131
- * (or null to create). Always rotates `activityGeneration` and resets `seenAt`.
132
- */
133
61
  export declare function applyAggregation(params: {
134
62
  strategy: AggregationStrategy;
135
63
  existing: Pick<NotificationDoc, 'count' | 'latestActorIds'> | null;
@@ -1 +1 @@
1
- {"version":3,"file":"delivery-ledger.d.ts","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"delivery-ledger.d.ts","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAgB,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAWnF,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,cAAc,GAAG,SAAS,GAAG,YAAY,CAAC;AAEjF,MAAM,MAAM,kBAAkB,GAAG,qBAAqB,CAAC;AAEvD,MAAM,WAAW,qBAAqB;IACpC,+GAA+G;IAC/G,mBAAmB,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxF;AACD,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,eAAe,CAAC;AAChE,MAAM,MAAM,oBAAoB,GAAG,cAAc,GAAG,kBAAkB,GAAG,cAAc,GAAG,OAAO,CAAC;AAElG,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,eAAe,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,oBAAoB,CAAC;CAC5C;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,SAAS,CAAA;CAAE,GAC1C;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAC5C;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,MAAM,kBAAkB,GAC1B,cAAc,GACd,sBAAsB,GACtB,8BAA8B,GAC9B,SAAS,GACT,oBAAoB,CAAC;AAEzB,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACjG;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAEzE,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1D,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC7D,eAAe,CACb,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC7E;AAYD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,GAAG,gBAAgB,CAAC,GAAG,IAAI,CAAC;IACnE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG;IACF,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAeA;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,wBAAwB,EAChC,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAkThB"}
@@ -1,64 +1,26 @@
1
- /**
2
- * Generic notification delivery ledger (notification redesign — P1).
3
- *
4
- * One Firestore doc per (recipient|shared, type, occurrence) is BOTH the queue
5
- * row AND the idempotency ledger. This package owns the GENERIC mechanism;
6
- * the consuming app owns the concrete collection name, the deterministic
7
- * `deliveryId`/`eventId`/`aggregationKey` construction, and the worker that
8
- * selects rows per `materializationClass`. The app passes fully-formed
9
- * `DeliveryRowInput`s; this module never invents domain ids.
10
- *
11
- * Key invariants (FROZEN — this module is their single source; the consuming app's
12
- * notification design doc explains the product rationale):
13
- * - Enqueue = create-if-absent. `ALREADY_EXISTS` is a per-row duplicate no-op,
14
- * never a failure — there is no marker-without-payload state, so the
15
- * "marker created, payload lost" crash window cannot exist.
16
- * - Materialize = ONE transaction that reads the delivery row + active card,
17
- * applies the aggregation exactly once (per strategy + caps), rotates the
18
- * opaque `activityGeneration`, resets `seenAt`, and flips the row to
19
- * `materialized` — so the "applied but not marked" double-apply window is gone.
20
- * Retrying a `materialized` row is a successful no-op.
21
- * - The same transaction asks the app whether the recipient may still receive
22
- * (`options.isRecipientEligible`); an ineligible recipient gets no card and the
23
- * row turns terminal `skipped`, so a row queued before (or retried after) the
24
- * recipient became ineligible can never recreate a card. Re-running it is a no-op.
25
- * - TTL (`expireAt`, a real Firestore Timestamp via `config.timestampFromMillis`)
26
- * is set ONLY at `materialized` / `skipped`; a `queued` or `deadLetter` row is NEVER TTL'd
27
- * so an unresolved delivery can't be silently deleted before replay.
28
- */
29
1
  import { randomUUID } from 'node:crypto';
30
2
  import { buildActiveNotificationDocId } from './activeNotificationId.js';
31
3
  const DEFAULT_DELIVERIES_PATH = 'notificationDeliveries';
32
- const DEFAULT_DELIVERY_TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 days
4
+ const DEFAULT_DELIVERY_TTL_MS = 90 * 24 * 60 * 60 * 1000;
33
5
  const DEFAULT_MAX_ATTEMPTS = 8;
34
6
  const DEFAULT_COUNT_CAP = 5000;
35
7
  const DEFAULT_ACTOR_CAP = 5;
36
- const BACKOFF_BASE_MS = 60 * 1000; // 1 min
37
- const BACKOFF_MAX_MS = 60 * 60 * 1000; // 1 hour
38
- /** gRPC ALREADY_EXISTS (6) — the create-if-absent duplicate signal. */
8
+ const BACKOFF_BASE_MS = 60 * 1000;
9
+ const BACKOFF_MAX_MS = 60 * 60 * 1000;
39
10
  function isAlreadyExists(error) {
40
11
  const e = error;
41
12
  return e?.code === 6 || /already.?exists/i.test(e?.message ?? '');
42
13
  }
43
14
  function backoffMs(attemptCount) {
44
15
  const exp = Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, attemptCount - 1), BACKOFF_MAX_MS);
45
- // Full jitter — spread retries so a burst of failures doesn't synchronize.
46
16
  return Math.floor(exp / 2 + Math.random() * (exp / 2));
47
17
  }
48
- /**
49
- * Apply the aggregation strategy to produce the active-doc field delta.
50
- * PURE — exported for direct unit testing. `existing` is the current active doc
51
- * (or null to create). Always rotates `activityGeneration` and resets `seenAt`.
52
- */
53
18
  export function applyAggregation(params) {
54
19
  const { strategy, existing, actorId, countCap, actorCap, buildMessage, now, generation } = params;
55
20
  const prevActors = existing?.latestActorIds ?? [];
56
- // N-M2: a system / no-actor occurrence (null or '') is never injected into the actor
57
- // list — it would render as a blank avatar / unresolvable uid in `latestActorIds`.
58
21
  const latestActorIds = actorId
59
22
  ? [actorId, ...prevActors.filter((id) => id !== actorId)].slice(0, actorCap)
60
23
  : prevActors.slice(0, actorCap);
61
- // staticRelight never shows a count (stays 1); increment counts up to the cap.
62
24
  const count = strategy === 'staticRelight' ? 1 : Math.min((existing?.count ?? 0) + 1, countCap);
63
25
  return {
64
26
  count,
@@ -78,8 +40,7 @@ export function createDeliveryLedger(db, config, options) {
78
40
  if (!configTimestampFromMillis) {
79
41
  throw new Error('[notification-core] createDeliveryLedger requires config.timestampFromMillis (Firestore TTL needs a real Timestamp for expireAt).');
80
42
  }
81
- // Bind to an explicitly non-optional type so the nested transaction closures
82
- // (called later) see it as defined, not `... | undefined`.
43
+ // An explicitly non-optional binding: TS does not carry the narrowing into closures called later.
83
44
  const toTimestamp = configTimestampFromMillis;
84
45
  const deliveries = db.collection(deliveriesPath);
85
46
  function deliveryRef(deliveryId) {
@@ -103,7 +64,6 @@ export function createDeliveryLedger(db, config, options) {
103
64
  createdAt: now,
104
65
  materializedAt: null,
105
66
  deadLetteredAt: null,
106
- // No expireAt while queued.
107
67
  };
108
68
  }
109
69
  function getTypeConfig(type) {
@@ -149,8 +109,6 @@ export function createDeliveryLedger(db, config, options) {
149
109
  const notificationType = d.notificationType;
150
110
  const aggregationKey = d.aggregationKey;
151
111
  const recipientUid = d.recipientUid ?? null;
152
- // The recipient is checked in this transaction, so a recipient that became ineligible
153
- // (e.g. an erased account) never gets a card from a row queued before, or retried after.
154
112
  if (recipientUid !== null && !(await isRecipientEligible(recipientUid, tx))) {
155
113
  const skippedAt = Date.now();
156
114
  tx.update(dRef, {
@@ -175,19 +133,14 @@ export function createDeliveryLedger(db, config, options) {
175
133
  dedupKey: aggregationKey,
176
134
  notificationType,
177
135
  }));
178
- const activeSnap = await tx.get(activeRef); // all reads before writes
136
+ const activeSnap = await tx.get(activeRef); // Firestore transactions take every read before any write.
179
137
  const now = Date.now();
180
138
  const generation = randomUUID();
181
139
  const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;
182
140
  const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;
183
141
  const buildMessage = (count) => typeConfig.messagePattern(payload.metadata, count);
184
- // Latest-occurrence-derived fields — recomputed every materialize so an EXISTING
185
- // aggregate card reflects the newest activity (title/targetPath/metadata), not the
186
- // first occurrence's stale values (N-I4).
187
142
  const title = typeConfig.titlePattern(payload.metadata);
188
- // A type with no defaultTargetPath is linkless: the doc must OMIT the field
189
- // entirely (Firestore rejects `undefined` values). A pre-existing doc written
190
- // before a type went linkless keeps its stale targetPath until archived.
143
+ // Firestore rejects `undefined` values, so a linkless type omits the field.
191
144
  const targetPath = typeConfig.defaultTargetPath === undefined
192
145
  ? undefined
193
146
  : typeof typeConfig.defaultTargetPath === 'function'
@@ -266,11 +219,6 @@ export function createDeliveryLedger(db, config, options) {
266
219
  });
267
220
  }
268
221
  else {
269
- // n1: re-stamp the row into the reserved `retry` materialization lane on backoff. Without
270
- // this the row keeps its original class and competes for that lane's capacity while backed
271
- // off; the worker's reserved `retry` slice (separate CLASS_CAPS capacity, drained in
272
- // DRAIN_ORDER) would always select 0 rows. Re-stamping moves a backed-off row into the
273
- // anti-starvation lane so it drains there instead of starving in its original lane.
274
222
  tx.update(dRef, {
275
223
  attemptCount,
276
224
  nextAttemptAt: now + backoffMs(attemptCount),
@@ -312,7 +260,7 @@ export function createDeliveryLedger(db, config, options) {
312
260
  nextAttemptAt: Date.now(),
313
261
  lastError: null,
314
262
  deadLetteredAt: null,
315
- expireAt: null, // clear any TTL set on a prior terminal
263
+ expireAt: null,
316
264
  });
317
265
  await options?.auditWrite?.(tx);
318
266
  return 'replayed';
@@ -1 +1 @@
1
- {"version":3,"file":"delivery-ledger.js","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generic notification delivery ledger (notification redesign — P1).\n *\n * One Firestore doc per (recipient|shared, type, occurrence) is BOTH the queue\n * row AND the idempotency ledger. This package owns the GENERIC mechanism;\n * the consuming app owns the concrete collection name, the deterministic\n * `deliveryId`/`eventId`/`aggregationKey` construction, and the worker that\n * selects rows per `materializationClass`. The app passes fully-formed\n * `DeliveryRowInput`s; this module never invents domain ids.\n *\n * Key invariants (FROZEN — this module is their single source; the consuming app's\n * notification design doc explains the product rationale):\n * - Enqueue = create-if-absent. `ALREADY_EXISTS` is a per-row duplicate no-op,\n * never a failure — there is no marker-without-payload state, so the\n * \"marker created, payload lost\" crash window cannot exist.\n * - Materialize = ONE transaction that reads the delivery row + active card,\n * applies the aggregation exactly once (per strategy + caps), rotates the\n * opaque `activityGeneration`, resets `seenAt`, and flips the row to\n * `materialized` — so the \"applied but not marked\" double-apply window is gone.\n * Retrying a `materialized` row is a successful no-op.\n * - The same transaction asks the app whether the recipient may still receive\n * (`options.isRecipientEligible`); an ineligible recipient gets no card and the\n * row turns terminal `skipped`, so a row queued before (or retried after) the\n * recipient became ineligible can never recreate a card. Re-running it is a no-op.\n * - TTL (`expireAt`, a real Firestore Timestamp via `config.timestampFromMillis`)\n * is set ONLY at `materialized` / `skipped`; a `queued` or `deadLetter` row is NEVER TTL'd\n * so an unresolved delivery can't be silently deleted before replay.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { NotificationSystemConfig, NotificationDoc } from '../types.js';\nimport type { ServerFirestore, ServerDocRef, ServerTransaction } from './types.js';\nimport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\nconst DEFAULT_DELIVERIES_PATH = 'notificationDeliveries';\nconst DEFAULT_DELIVERY_TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 days\nconst DEFAULT_MAX_ATTEMPTS = 8;\nconst DEFAULT_COUNT_CAP = 5000;\nconst DEFAULT_ACTOR_CAP = 5;\nconst BACKOFF_BASE_MS = 60 * 1000; // 1 min\nconst BACKOFF_MAX_MS = 60 * 60 * 1000; // 1 hour\n\n/**\n * `skipped` is terminal: the recipient was not eligible when the row was materialized, so no card\n * was written. It is TTL'd like `materialized` and never retried.\n */\nexport type DeliveryState = 'queued' | 'materialized' | 'skipped' | 'deadLetter';\n\n/** Why a row was terminalized as `skipped`. */\nexport type DeliverySkipReason = 'recipientIneligible';\n\n/**\n * Ledger options the consuming app must decide.\n */\nexport interface DeliveryLedgerOptions {\n /**\n * Whether `recipientUid` may still receive a notification. Called INSIDE the materialize\n * transaction, after the delivery row is read and before anything is written, so the answer\n * and the card write commit together (BACKEND-116): read through `tx` only. A recipient that\n * answers false gets no card, and the row becomes `skipped`. Not called for a shared\n * (recipient-less) row. A consumer with no eligibility rule answers true.\n */\n isRecipientEligible: (recipientUid: string, tx: ServerTransaction) => Promise<boolean>;\n}\nexport type AggregationStrategy = 'increment' | 'staticRelight';\nexport type MaterializationClass = 'directQueued' | 'realtimeFallback' | 'fanoutOrphan' | 'retry';\n\n/** The materialization payload carried on every delivery row. */\nexport interface DeliveryPayload {\n /** The acting user's uid, or `null`/`''` for a system / no-actor occurrence (N-M2). */\n actorId: string | null;\n metadata: Record<string, unknown>;\n occurrenceAt: number;\n}\n\n/** App-supplied, fully-formed delivery row (the app owns all id construction). */\nexport interface DeliveryRowInput {\n deliveryId: string;\n notificationType: string;\n eventId: string;\n /** `null` for shared-admin occurrences (the deliveryId hash uses the literal `'shared'`). */\n recipientUid: string | null;\n aggregationKey: string;\n strategy: AggregationStrategy;\n payload: DeliveryPayload;\n payloadVersion: number;\n materializationClass: MaterializationClass;\n}\n\nexport type EnqueueRowResult =\n | { deliveryId: string; outcome: 'created' }\n | { deliveryId: string; outcome: 'duplicate' }\n | { deliveryId: string; outcome: 'failed'; error: unknown };\n\nexport interface EnqueueResult {\n results: EnqueueRowResult[];\n /** True iff EVERY row is now represented by a created-or-pre-existing doc (page-cursor invariant input). */\n allResolved: boolean;\n}\n\nexport type MaterializeOutcome =\n | 'materialized'\n | 'already-materialized'\n | 'skipped-ineligible-recipient'\n | 'missing'\n | 'skipped-non-queued';\n\nexport interface MaterializeManyOptions {\n /** Rows materialized at once (default 10). */\n concurrency?: number;\n /**\n * Called for a row whose materialize threw and whose failure could not then be recorded either —\n * `error` is the recording's own failure and `details.cause` the materialize failure it was\n * recording. The row stays as it was (queued, attempt count\n * unchanged), so a later run selects it again; the rest of the batch proceeds. The app's capture\n * goes here; without it the ledger writes the failure to `console.error`.\n */\n onUnrecordedFailure?: (deliveryId: string, error: unknown, details: { cause: unknown }) => void;\n}\n\n/** What a `replay` did: re-queued the row, found no row, or found a row that is not dead-lettered. */\nexport type ReplayOutcome = 'replayed' | 'missing' | 'not-dead-lettered';\n\nexport interface ReplayOptions {\n /**\n * The caller's own write (its audit event), composed into the replay transaction and awaited\n * there on the `replayed` path only. A hook that throws or rejects fails the replay, so the\n * reset never commits without it.\n */\n auditWrite?: (txn: ServerTransaction) => void | Promise<void>;\n}\n\nexport interface DeliveryLedger {\n enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult>;\n materialize(deliveryId: string): Promise<MaterializeOutcome>;\n /**\n * Materialize many rows with bounded concurrency. A row whose materialize throws has the failure\n * recorded (a retry or a dead letter); each row is isolated, so one row's failure — even one\n * whose recording also fails — never fails the batch or stops the rows behind it.\n */\n materializeMany(\n deliveryIds: string[],\n options?: MaterializeManyOptions,\n ): Promise<Record<MaterializeOutcome, number>>;\n recordTransientFailure(deliveryId: string, error: unknown): Promise<void>;\n deadLetter(deliveryId: string, error: unknown): Promise<void>;\n /** Re-queue a dead-lettered row. A missing or non-dead-lettered row is answered, never written. */\n replay(deliveryId: string, options?: ReplayOptions): Promise<ReplayOutcome>;\n}\n\n/** gRPC ALREADY_EXISTS (6) — the create-if-absent duplicate signal. */\nfunction isAlreadyExists(error: unknown): boolean {\n const e = error as { code?: number; message?: string } | undefined;\n return e?.code === 6 || /already.?exists/i.test(e?.message ?? '');\n}\n\nfunction backoffMs(attemptCount: number): number {\n const exp = Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, attemptCount - 1), BACKOFF_MAX_MS);\n // Full jitter — spread retries so a burst of failures doesn't synchronize.\n return Math.floor(exp / 2 + Math.random() * (exp / 2));\n}\n\n/**\n * Apply the aggregation strategy to produce the active-doc field delta.\n * PURE — exported for direct unit testing. `existing` is the current active doc\n * (or null to create). Always rotates `activityGeneration` and resets `seenAt`.\n */\nexport function applyAggregation(params: {\n strategy: AggregationStrategy;\n existing: Pick<NotificationDoc, 'count' | 'latestActorIds'> | null;\n actorId: string | null;\n countCap: number;\n actorCap: number;\n buildMessage: (count: number) => string;\n now: number;\n generation: string;\n}): {\n count: number;\n latestActorIds: string[];\n message: string;\n activityGeneration: string;\n seenAt: number;\n updatedAt: number;\n} {\n const { strategy, existing, actorId, countCap, actorCap, buildMessage, now, generation } = params;\n const prevActors = existing?.latestActorIds ?? [];\n // N-M2: a system / no-actor occurrence (null or '') is never injected into the actor\n // list — it would render as a blank avatar / unresolvable uid in `latestActorIds`.\n const latestActorIds = actorId\n ? [actorId, ...prevActors.filter((id) => id !== actorId)].slice(0, actorCap)\n : prevActors.slice(0, actorCap);\n // staticRelight never shows a count (stays 1); increment counts up to the cap.\n const count = strategy === 'staticRelight' ? 1 : Math.min((existing?.count ?? 0) + 1, countCap);\n return {\n count,\n latestActorIds,\n message: buildMessage(count),\n activityGeneration: generation,\n seenAt: 0,\n updatedAt: now,\n };\n}\n\nexport function createDeliveryLedger(\n db: ServerFirestore,\n config: NotificationSystemConfig,\n options: DeliveryLedgerOptions,\n): DeliveryLedger {\n const { isRecipientEligible } = options;\n const deliveriesPath = config.deliveriesCollectionPath ?? DEFAULT_DELIVERIES_PATH;\n const ttlMs = config.deliveryTtlMs ?? DEFAULT_DELIVERY_TTL_MS;\n const maxAttempts = config.maxDeliveryAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const configTimestampFromMillis = config.timestampFromMillis;\n if (!configTimestampFromMillis) {\n throw new Error(\n '[notification-core] createDeliveryLedger requires config.timestampFromMillis (Firestore TTL needs a real Timestamp for expireAt).',\n );\n }\n // Bind to an explicitly non-optional type so the nested transaction closures\n // (called later) see it as defined, not `... | undefined`.\n const toTimestamp: (ms: number) => unknown = configTimestampFromMillis;\n\n const deliveries = db.collection(deliveriesPath);\n\n function deliveryRef(deliveryId: string): ServerDocRef {\n return deliveries.doc(deliveryId);\n }\n\n function buildDeliveryDoc(row: DeliveryRowInput, now: number): Record<string, unknown> {\n return {\n deliveryId: row.deliveryId,\n state: 'queued' as DeliveryState,\n notificationType: row.notificationType,\n eventId: row.eventId,\n recipientUid: row.recipientUid,\n aggregationKey: row.aggregationKey,\n strategy: row.strategy,\n payload: row.payload,\n payloadVersion: row.payloadVersion,\n materializationClass: row.materializationClass,\n attemptCount: 0,\n nextAttemptAt: now,\n lastError: null,\n createdAt: now,\n materializedAt: null,\n deadLetteredAt: null,\n // No expireAt while queued.\n };\n }\n\n function getTypeConfig(type: string) {\n const typeConfig = config.types[type];\n if (!typeConfig) throw new Error(`[notification-core] Unknown notification type: ${type}`);\n const categoryConfig = config.categories[typeConfig.category];\n if (!categoryConfig)\n throw new Error(`[notification-core] Unknown category: ${typeConfig.category}`);\n return { typeConfig, categoryConfig };\n }\n\n async function enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult> {\n if (rows.length === 0) return { results: [], allResolved: true };\n const now = Date.now();\n const settled = await Promise.allSettled(\n rows.map((row) =>\n deliveryRef(row.deliveryId)\n .create(buildDeliveryDoc(row, now))\n .then(() => ({ deliveryId: row.deliveryId, outcome: 'created' as const })),\n ),\n );\n const results: EnqueueRowResult[] = settled.map((s, i) => {\n const deliveryId = rows[i].deliveryId;\n if (s.status === 'fulfilled') return s.value;\n if (isAlreadyExists(s.reason)) return { deliveryId, outcome: 'duplicate' };\n return { deliveryId, outcome: 'failed', error: s.reason };\n });\n return { results, allResolved: results.every((r) => r.outcome !== 'failed') };\n }\n\n async function materialize(deliveryId: string): Promise<MaterializeOutcome> {\n return db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return 'missing';\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n const state = d.state as DeliveryState;\n if (state === 'materialized') return 'already-materialized';\n if (state === 'skipped') return 'skipped-ineligible-recipient';\n if (state !== 'queued') return 'skipped-non-queued';\n\n const notificationType = d.notificationType as string;\n const aggregationKey = d.aggregationKey as string;\n const recipientUid = (d.recipientUid as string | null) ?? null;\n\n // The recipient is checked in this transaction, so a recipient that became ineligible\n // (e.g. an erased account) never gets a card from a row queued before, or retried after.\n if (recipientUid !== null && !(await isRecipientEligible(recipientUid, tx))) {\n const skippedAt = Date.now();\n tx.update(dRef, {\n state: 'skipped' as DeliveryState,\n skipReason: 'recipientIneligible' as DeliverySkipReason,\n skippedAt,\n expireAt: toTimestamp(skippedAt + ttlMs),\n });\n return 'skipped-ineligible-recipient';\n }\n const strategy = (d.strategy as AggregationStrategy) ?? 'increment';\n const payload = (d.payload as DeliveryPayload) ?? {\n actorId: '',\n metadata: {},\n occurrenceAt: Date.now(),\n };\n const { typeConfig, categoryConfig } = getTypeConfig(notificationType);\n\n const activeRef = db.collection(categoryConfig.activePath).doc(\n buildActiveNotificationDocId({\n category: typeConfig.category,\n audienceType: categoryConfig.audienceType,\n targetUserId: recipientUid,\n dedupKey: aggregationKey,\n notificationType,\n }),\n );\n const activeSnap = await tx.get(activeRef); // all reads before writes\n\n const now = Date.now();\n const generation = randomUUID();\n const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;\n const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;\n const buildMessage = (count: number) => typeConfig.messagePattern(payload.metadata, count);\n // Latest-occurrence-derived fields — recomputed every materialize so an EXISTING\n // aggregate card reflects the newest activity (title/targetPath/metadata), not the\n // first occurrence's stale values (N-I4).\n const title = typeConfig.titlePattern(payload.metadata);\n // A type with no defaultTargetPath is linkless: the doc must OMIT the field\n // entirely (Firestore rejects `undefined` values). A pre-existing doc written\n // before a type went linkless keeps its stale targetPath until archived.\n const targetPath =\n typeConfig.defaultTargetPath === undefined\n ? undefined\n : typeof typeConfig.defaultTargetPath === 'function'\n ? typeConfig.defaultTargetPath(payload.metadata)\n : typeConfig.defaultTargetPath;\n const targetPathField = targetPath === undefined ? {} : { targetPath };\n\n if (activeSnap.exists) {\n const existing = (activeSnap.data() ?? {}) as Pick<\n NotificationDoc,\n 'count' | 'latestActorIds'\n >;\n const delta = applyAggregation({\n strategy,\n existing,\n actorId: payload.actorId,\n countCap,\n actorCap,\n buildMessage,\n now,\n generation,\n });\n tx.update(activeRef, { ...delta, title, ...targetPathField, metadata: payload.metadata });\n } else {\n const delta = applyAggregation({\n strategy,\n existing: null,\n actorId: payload.actorId,\n countCap,\n actorCap,\n buildMessage,\n now,\n generation,\n });\n const newDoc: Omit<NotificationDoc, 'id'> = {\n type: notificationType,\n dedupKey: aggregationKey,\n category: typeConfig.category,\n targetUserId: recipientUid,\n title,\n message: delta.message,\n count: delta.count,\n latestActorIds: delta.latestActorIds,\n ...targetPathField,\n metadata: payload.metadata,\n seenAt: 0,\n activityGeneration: generation,\n createdAt: now,\n updatedAt: now,\n };\n tx.set(activeRef, newDoc as Record<string, unknown>);\n }\n\n tx.update(dRef, {\n state: 'materialized' as DeliveryState,\n materializedAt: now,\n expireAt: toTimestamp(now + ttlMs),\n });\n return 'materialized';\n });\n }\n\n async function recordTransientFailure(deliveryId: string, error: unknown): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'queued') return;\n const attemptCount = ((d.attemptCount as number) ?? 0) + 1;\n const now = Date.now();\n const message = error instanceof Error ? error.message : String(error);\n if (attemptCount >= maxAttempts) {\n tx.update(dRef, {\n state: 'deadLetter' as DeliveryState,\n attemptCount,\n lastError: message,\n deadLetteredAt: now,\n });\n } else {\n // n1: re-stamp the row into the reserved `retry` materialization lane on backoff. Without\n // this the row keeps its original class and competes for that lane's capacity while backed\n // off; the worker's reserved `retry` slice (separate CLASS_CAPS capacity, drained in\n // DRAIN_ORDER) would always select 0 rows. Re-stamping moves a backed-off row into the\n // anti-starvation lane so it drains there instead of starving in its original lane.\n tx.update(dRef, {\n attemptCount,\n nextAttemptAt: now + backoffMs(attemptCount),\n lastError: message,\n materializationClass: 'retry' as MaterializationClass,\n });\n }\n });\n }\n\n async function deadLetter(deliveryId: string, error: unknown): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'queued') return;\n tx.update(dRef, {\n state: 'deadLetter' as DeliveryState,\n attemptCount: ((d.attemptCount as number) ?? 0) + 1,\n lastError: error instanceof Error ? error.message : String(error),\n deadLetteredAt: Date.now(),\n });\n });\n }\n\n async function replay(deliveryId: string, options?: ReplayOptions): Promise<ReplayOutcome> {\n return db.runTransaction(async (tx): Promise<ReplayOutcome> => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return 'missing';\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'deadLetter') return 'not-dead-lettered';\n tx.update(dRef, {\n state: 'queued' as DeliveryState,\n attemptCount: 0,\n nextAttemptAt: Date.now(),\n lastError: null,\n deadLetteredAt: null,\n expireAt: null, // clear any TTL set on a prior terminal\n });\n await options?.auditWrite?.(tx);\n return 'replayed';\n });\n }\n\n function reportUnrecordedFailure(\n options: MaterializeManyOptions | undefined,\n deliveryId: string,\n error: unknown,\n cause: unknown,\n ): void {\n try {\n if (options?.onUnrecordedFailure) {\n options.onUnrecordedFailure(deliveryId, error, { cause });\n return;\n }\n } catch (handlerError) {\n console.error(\n '[notification-core] onUnrecordedFailure threw for delivery',\n deliveryId,\n handlerError,\n );\n }\n console.error(\n '[notification-core] A delivery failure could not be recorded',\n deliveryId,\n error,\n { cause },\n );\n }\n\n async function materializeMany(\n deliveryIds: string[],\n options?: MaterializeManyOptions,\n ): Promise<Record<MaterializeOutcome, number>> {\n const concurrency = Math.max(1, options?.concurrency ?? 10);\n const tally: Record<MaterializeOutcome, number> = {\n materialized: 0,\n 'already-materialized': 0,\n 'skipped-ineligible-recipient': 0,\n missing: 0,\n 'skipped-non-queued': 0,\n };\n let cursor = 0;\n async function worker(): Promise<void> {\n while (cursor < deliveryIds.length) {\n const id = deliveryIds[cursor++];\n try {\n const outcome = await materialize(id);\n tally[outcome] += 1;\n } catch (error) {\n try {\n await recordTransientFailure(id, error);\n } catch (recordError) {\n reportUnrecordedFailure(options, id, recordError, error);\n }\n }\n }\n }\n await Promise.all(Array.from({ length: Math.min(concurrency, deliveryIds.length) }, worker));\n return tally;\n }\n\n return { enqueue, materialize, materializeMany, recordTransientFailure, deadLetter, replay };\n}\n"]}
1
+ {"version":3,"file":"delivery-ledger.js","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { NotificationSystemConfig, NotificationDoc } from '../types.js';\nimport type { ServerFirestore, ServerDocRef, ServerTransaction } from './types.js';\nimport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\nconst DEFAULT_DELIVERIES_PATH = 'notificationDeliveries';\nconst DEFAULT_DELIVERY_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst DEFAULT_MAX_ATTEMPTS = 8;\nconst DEFAULT_COUNT_CAP = 5000;\nconst DEFAULT_ACTOR_CAP = 5;\nconst BACKOFF_BASE_MS = 60 * 1000;\nconst BACKOFF_MAX_MS = 60 * 60 * 1000;\n\nexport type DeliveryState = 'queued' | 'materialized' | 'skipped' | 'deadLetter';\n\nexport type DeliverySkipReason = 'recipientIneligible';\n\nexport interface DeliveryLedgerOptions {\n /** Runs inside the materialize transaction before any write: read through `tx` only (RULE-STD-BACKEND-116). */\n isRecipientEligible: (recipientUid: string, tx: ServerTransaction) => Promise<boolean>;\n}\nexport type AggregationStrategy = 'increment' | 'staticRelight';\nexport type MaterializationClass = 'directQueued' | 'realtimeFallback' | 'fanoutOrphan' | 'retry';\n\nexport interface DeliveryPayload {\n actorId: string | null;\n metadata: Record<string, unknown>;\n occurrenceAt: number;\n}\n\nexport interface DeliveryRowInput {\n deliveryId: string;\n notificationType: string;\n eventId: string;\n recipientUid: string | null;\n aggregationKey: string;\n strategy: AggregationStrategy;\n payload: DeliveryPayload;\n payloadVersion: number;\n materializationClass: MaterializationClass;\n}\n\nexport type EnqueueRowResult =\n | { deliveryId: string; outcome: 'created' }\n | { deliveryId: string; outcome: 'duplicate' }\n | { deliveryId: string; outcome: 'failed'; error: unknown };\n\nexport interface EnqueueResult {\n results: EnqueueRowResult[];\n allResolved: boolean;\n}\n\nexport type MaterializeOutcome =\n | 'materialized'\n | 'already-materialized'\n | 'skipped-ineligible-recipient'\n | 'missing'\n | 'skipped-non-queued';\n\nexport interface MaterializeManyOptions {\n concurrency?: number;\n onUnrecordedFailure?: (deliveryId: string, error: unknown, details: { cause: unknown }) => void;\n}\n\nexport type ReplayOutcome = 'replayed' | 'missing' | 'not-dead-lettered';\n\nexport interface ReplayOptions {\n auditWrite?: (txn: ServerTransaction) => void | Promise<void>;\n}\n\nexport interface DeliveryLedger {\n enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult>;\n materialize(deliveryId: string): Promise<MaterializeOutcome>;\n materializeMany(\n deliveryIds: string[],\n options?: MaterializeManyOptions,\n ): Promise<Record<MaterializeOutcome, number>>;\n recordTransientFailure(deliveryId: string, error: unknown): Promise<void>;\n deadLetter(deliveryId: string, error: unknown): Promise<void>;\n replay(deliveryId: string, options?: ReplayOptions): Promise<ReplayOutcome>;\n}\n\nfunction isAlreadyExists(error: unknown): boolean {\n const e = error as { code?: number; message?: string } | undefined;\n return e?.code === 6 || /already.?exists/i.test(e?.message ?? '');\n}\n\nfunction backoffMs(attemptCount: number): number {\n const exp = Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, attemptCount - 1), BACKOFF_MAX_MS);\n return Math.floor(exp / 2 + Math.random() * (exp / 2));\n}\n\nexport function applyAggregation(params: {\n strategy: AggregationStrategy;\n existing: Pick<NotificationDoc, 'count' | 'latestActorIds'> | null;\n actorId: string | null;\n countCap: number;\n actorCap: number;\n buildMessage: (count: number) => string;\n now: number;\n generation: string;\n}): {\n count: number;\n latestActorIds: string[];\n message: string;\n activityGeneration: string;\n seenAt: number;\n updatedAt: number;\n} {\n const { strategy, existing, actorId, countCap, actorCap, buildMessage, now, generation } = params;\n const prevActors = existing?.latestActorIds ?? [];\n const latestActorIds = actorId\n ? [actorId, ...prevActors.filter((id) => id !== actorId)].slice(0, actorCap)\n : prevActors.slice(0, actorCap);\n const count = strategy === 'staticRelight' ? 1 : Math.min((existing?.count ?? 0) + 1, countCap);\n return {\n count,\n latestActorIds,\n message: buildMessage(count),\n activityGeneration: generation,\n seenAt: 0,\n updatedAt: now,\n };\n}\n\nexport function createDeliveryLedger(\n db: ServerFirestore,\n config: NotificationSystemConfig,\n options: DeliveryLedgerOptions,\n): DeliveryLedger {\n const { isRecipientEligible } = options;\n const deliveriesPath = config.deliveriesCollectionPath ?? DEFAULT_DELIVERIES_PATH;\n const ttlMs = config.deliveryTtlMs ?? DEFAULT_DELIVERY_TTL_MS;\n const maxAttempts = config.maxDeliveryAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const configTimestampFromMillis = config.timestampFromMillis;\n if (!configTimestampFromMillis) {\n throw new Error(\n '[notification-core] createDeliveryLedger requires config.timestampFromMillis (Firestore TTL needs a real Timestamp for expireAt).',\n );\n }\n // An explicitly non-optional binding: TS does not carry the narrowing into closures called later.\n const toTimestamp: (ms: number) => unknown = configTimestampFromMillis;\n\n const deliveries = db.collection(deliveriesPath);\n\n function deliveryRef(deliveryId: string): ServerDocRef {\n return deliveries.doc(deliveryId);\n }\n\n function buildDeliveryDoc(row: DeliveryRowInput, now: number): Record<string, unknown> {\n return {\n deliveryId: row.deliveryId,\n state: 'queued' as DeliveryState,\n notificationType: row.notificationType,\n eventId: row.eventId,\n recipientUid: row.recipientUid,\n aggregationKey: row.aggregationKey,\n strategy: row.strategy,\n payload: row.payload,\n payloadVersion: row.payloadVersion,\n materializationClass: row.materializationClass,\n attemptCount: 0,\n nextAttemptAt: now,\n lastError: null,\n createdAt: now,\n materializedAt: null,\n deadLetteredAt: null,\n };\n }\n\n function getTypeConfig(type: string) {\n const typeConfig = config.types[type];\n if (!typeConfig) throw new Error(`[notification-core] Unknown notification type: ${type}`);\n const categoryConfig = config.categories[typeConfig.category];\n if (!categoryConfig)\n throw new Error(`[notification-core] Unknown category: ${typeConfig.category}`);\n return { typeConfig, categoryConfig };\n }\n\n async function enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult> {\n if (rows.length === 0) return { results: [], allResolved: true };\n const now = Date.now();\n const settled = await Promise.allSettled(\n rows.map((row) =>\n deliveryRef(row.deliveryId)\n .create(buildDeliveryDoc(row, now))\n .then(() => ({ deliveryId: row.deliveryId, outcome: 'created' as const })),\n ),\n );\n const results: EnqueueRowResult[] = settled.map((s, i) => {\n const deliveryId = rows[i].deliveryId;\n if (s.status === 'fulfilled') return s.value;\n if (isAlreadyExists(s.reason)) return { deliveryId, outcome: 'duplicate' };\n return { deliveryId, outcome: 'failed', error: s.reason };\n });\n return { results, allResolved: results.every((r) => r.outcome !== 'failed') };\n }\n\n async function materialize(deliveryId: string): Promise<MaterializeOutcome> {\n return db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return 'missing';\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n const state = d.state as DeliveryState;\n if (state === 'materialized') return 'already-materialized';\n if (state === 'skipped') return 'skipped-ineligible-recipient';\n if (state !== 'queued') return 'skipped-non-queued';\n\n const notificationType = d.notificationType as string;\n const aggregationKey = d.aggregationKey as string;\n const recipientUid = (d.recipientUid as string | null) ?? null;\n\n if (recipientUid !== null && !(await isRecipientEligible(recipientUid, tx))) {\n const skippedAt = Date.now();\n tx.update(dRef, {\n state: 'skipped' as DeliveryState,\n skipReason: 'recipientIneligible' as DeliverySkipReason,\n skippedAt,\n expireAt: toTimestamp(skippedAt + ttlMs),\n });\n return 'skipped-ineligible-recipient';\n }\n const strategy = (d.strategy as AggregationStrategy) ?? 'increment';\n const payload = (d.payload as DeliveryPayload) ?? {\n actorId: '',\n metadata: {},\n occurrenceAt: Date.now(),\n };\n const { typeConfig, categoryConfig } = getTypeConfig(notificationType);\n\n const activeRef = db.collection(categoryConfig.activePath).doc(\n buildActiveNotificationDocId({\n category: typeConfig.category,\n audienceType: categoryConfig.audienceType,\n targetUserId: recipientUid,\n dedupKey: aggregationKey,\n notificationType,\n }),\n );\n const activeSnap = await tx.get(activeRef); // Firestore transactions take every read before any write.\n\n const now = Date.now();\n const generation = randomUUID();\n const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;\n const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;\n const buildMessage = (count: number) => typeConfig.messagePattern(payload.metadata, count);\n const title = typeConfig.titlePattern(payload.metadata);\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(payload.metadata)\n : typeConfig.defaultTargetPath;\n const targetPathField = targetPath === undefined ? {} : { targetPath };\n\n if (activeSnap.exists) {\n const existing = (activeSnap.data() ?? {}) as Pick<\n NotificationDoc,\n 'count' | 'latestActorIds'\n >;\n const delta = applyAggregation({\n strategy,\n existing,\n actorId: payload.actorId,\n countCap,\n actorCap,\n buildMessage,\n now,\n generation,\n });\n tx.update(activeRef, { ...delta, title, ...targetPathField, metadata: payload.metadata });\n } else {\n const delta = applyAggregation({\n strategy,\n existing: null,\n actorId: payload.actorId,\n countCap,\n actorCap,\n buildMessage,\n now,\n generation,\n });\n const newDoc: Omit<NotificationDoc, 'id'> = {\n type: notificationType,\n dedupKey: aggregationKey,\n category: typeConfig.category,\n targetUserId: recipientUid,\n title,\n message: delta.message,\n count: delta.count,\n latestActorIds: delta.latestActorIds,\n ...targetPathField,\n metadata: payload.metadata,\n seenAt: 0,\n activityGeneration: generation,\n createdAt: now,\n updatedAt: now,\n };\n tx.set(activeRef, newDoc as Record<string, unknown>);\n }\n\n tx.update(dRef, {\n state: 'materialized' as DeliveryState,\n materializedAt: now,\n expireAt: toTimestamp(now + ttlMs),\n });\n return 'materialized';\n });\n }\n\n async function recordTransientFailure(deliveryId: string, error: unknown): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'queued') return;\n const attemptCount = ((d.attemptCount as number) ?? 0) + 1;\n const now = Date.now();\n const message = error instanceof Error ? error.message : String(error);\n if (attemptCount >= maxAttempts) {\n tx.update(dRef, {\n state: 'deadLetter' as DeliveryState,\n attemptCount,\n lastError: message,\n deadLetteredAt: now,\n });\n } else {\n tx.update(dRef, {\n attemptCount,\n nextAttemptAt: now + backoffMs(attemptCount),\n lastError: message,\n materializationClass: 'retry' as MaterializationClass,\n });\n }\n });\n }\n\n async function deadLetter(deliveryId: string, error: unknown): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'queued') return;\n tx.update(dRef, {\n state: 'deadLetter' as DeliveryState,\n attemptCount: ((d.attemptCount as number) ?? 0) + 1,\n lastError: error instanceof Error ? error.message : String(error),\n deadLetteredAt: Date.now(),\n });\n });\n }\n\n async function replay(deliveryId: string, options?: ReplayOptions): Promise<ReplayOutcome> {\n return db.runTransaction(async (tx): Promise<ReplayOutcome> => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return 'missing';\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'deadLetter') return 'not-dead-lettered';\n tx.update(dRef, {\n state: 'queued' as DeliveryState,\n attemptCount: 0,\n nextAttemptAt: Date.now(),\n lastError: null,\n deadLetteredAt: null,\n expireAt: null,\n });\n await options?.auditWrite?.(tx);\n return 'replayed';\n });\n }\n\n function reportUnrecordedFailure(\n options: MaterializeManyOptions | undefined,\n deliveryId: string,\n error: unknown,\n cause: unknown,\n ): void {\n try {\n if (options?.onUnrecordedFailure) {\n options.onUnrecordedFailure(deliveryId, error, { cause });\n return;\n }\n } catch (handlerError) {\n console.error(\n '[notification-core] onUnrecordedFailure threw for delivery',\n deliveryId,\n handlerError,\n );\n }\n console.error(\n '[notification-core] A delivery failure could not be recorded',\n deliveryId,\n error,\n { cause },\n );\n }\n\n async function materializeMany(\n deliveryIds: string[],\n options?: MaterializeManyOptions,\n ): Promise<Record<MaterializeOutcome, number>> {\n const concurrency = Math.max(1, options?.concurrency ?? 10);\n const tally: Record<MaterializeOutcome, number> = {\n materialized: 0,\n 'already-materialized': 0,\n 'skipped-ineligible-recipient': 0,\n missing: 0,\n 'skipped-non-queued': 0,\n };\n let cursor = 0;\n async function worker(): Promise<void> {\n while (cursor < deliveryIds.length) {\n const id = deliveryIds[cursor++];\n try {\n const outcome = await materialize(id);\n tally[outcome] += 1;\n } catch (error) {\n try {\n await recordTransientFailure(id, error);\n } catch (recordError) {\n reportUnrecordedFailure(options, id, recordError, error);\n }\n }\n }\n }\n await Promise.all(Array.from({ length: Math.min(concurrency, deliveryIds.length) }, worker));\n return tally;\n }\n\n return { enqueue, materialize, materializeMany, recordTransientFailure, deadLetter, replay };\n}\n"]}
@@ -1,12 +1,3 @@
1
- /**
2
- * Typed errors thrown by notification-core server helpers.
3
- */
4
- /**
5
- * Thrown when a caller is not allowed to perform a notification action — e.g.
6
- * archiving a personal notification they do not own, or a non-admin archiving a
7
- * shared notification. Consuming callables should catch this and map it to a
8
- * `permission-denied` error code rather than surfacing a generic `internal`.
9
- */
10
1
  export declare class NotificationPermissionError extends Error {
11
2
  constructor(message: string);
12
3
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/server/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;GAKG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAO5B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/server/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAM5B"}
@@ -1,18 +1,8 @@
1
- /**
2
- * Typed errors thrown by notification-core server helpers.
3
- */
4
- /**
5
- * Thrown when a caller is not allowed to perform a notification action — e.g.
6
- * archiving a personal notification they do not own, or a non-admin archiving a
7
- * shared notification. Consuming callables should catch this and map it to a
8
- * `permission-denied` error code rather than surfacing a generic `internal`.
9
- */
10
1
  export class NotificationPermissionError extends Error {
11
2
  constructor(message) {
12
3
  super(message);
13
4
  this.name = 'NotificationPermissionError';
14
- // Restore the prototype chain so `instanceof` works across the transpile
15
- // target (extending built-ins can break it otherwise).
5
+ // Restores the prototype chain, which extending a built-in can break under the transpile target.
16
6
  Object.setPrototypeOf(this, NotificationPermissionError.prototype);
17
7
  }
18
8
  }
@@ -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,GACxB,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAE3E;;;;;;;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"}