@ttt-productions/notification-core 0.7.0 → 0.8.1
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.
- package/dist/server/activeNotificationId.d.ts +7 -0
- package/dist/server/activeNotificationId.d.ts.map +1 -1
- package/dist/server/activeNotificationId.js +0 -0
- package/dist/server/activeNotificationId.js.map +1 -1
- package/dist/server/delivery-ledger.d.ts +99 -0
- package/dist/server/delivery-ledger.d.ts.map +1 -0
- package/dist/server/delivery-ledger.js +299 -0
- package/dist/server/delivery-ledger.js.map +1 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/observed-generation.d.ts +51 -0
- package/dist/server/observed-generation.d.ts.map +1 -0
- package/dist/server/observed-generation.js +80 -0
- package/dist/server/observed-generation.js.map +1 -0
- package/dist/server/types.d.ts +2 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -19,5 +19,12 @@ export declare function buildActiveNotificationDocId(params: {
|
|
|
19
19
|
audienceType: 'personal' | 'shared';
|
|
20
20
|
targetUserId: string | null | undefined;
|
|
21
21
|
dedupKey: string;
|
|
22
|
+
/**
|
|
23
|
+
* When provided, the id becomes **type-scoped** so two notification types that
|
|
24
|
+
* share an aggregation key never collapse onto one active doc (the delivery
|
|
25
|
+
* ledger materializer always passes it). Omitted => the legacy non-type-scoped
|
|
26
|
+
* id, kept byte-identical for the pre-cutover composer path.
|
|
27
|
+
*/
|
|
28
|
+
notificationType?: string;
|
|
22
29
|
}): string;
|
|
23
30
|
//# sourceMappingURL=activeNotificationId.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activeNotificationId.d.ts","sourceRoot":"","sources":["../../src/server/activeNotificationId.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,UAAU,GAAG,QAAQ,CAAC;IACpC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"activeNotificationId.d.ts","sourceRoot":"","sources":["../../src/server/activeNotificationId.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,UAAU,GAAG,QAAQ,CAAC;IACpC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,MAAM,CAQT"}
|
|
Binary file
|
|
@@ -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,
|
|
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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 in NOTIFICATIONS_REDESIGN):
|
|
12
|
+
* - Enqueue = create-if-absent. `ALREADY_EXISTS` is a per-row duplicate no-op,
|
|
13
|
+
* never a failure — there is no marker-without-payload state, so the
|
|
14
|
+
* "marker created, payload lost" crash window cannot exist.
|
|
15
|
+
* - Materialize = ONE transaction that reads the delivery row + active card,
|
|
16
|
+
* applies the aggregation exactly once (per strategy + caps), rotates the
|
|
17
|
+
* opaque `activityGeneration`, resets `seenAt`, and flips the row to
|
|
18
|
+
* `materialized` — so the "applied but not marked" double-apply window is gone.
|
|
19
|
+
* Retrying a `materialized` row is a successful no-op.
|
|
20
|
+
* - TTL (`expireAt`, a real Firestore Timestamp via `config.timestampFromMillis`)
|
|
21
|
+
* is set ONLY at `materialized`; a `queued` or `deadLetter` row is NEVER TTL'd
|
|
22
|
+
* (round-19) so an unresolved delivery can't be silently deleted before replay.
|
|
23
|
+
*/
|
|
24
|
+
import type { NotificationSystemConfig, NotificationDoc } from '../types.js';
|
|
25
|
+
import type { ServerFirestore } from './types.js';
|
|
26
|
+
export type DeliveryState = 'queued' | 'materialized' | 'deadLetter';
|
|
27
|
+
export type AggregationStrategy = 'increment' | 'staticRelight';
|
|
28
|
+
export type MaterializationClass = 'directQueued' | 'realtimeFallback' | 'fanoutOrphan' | 'retry';
|
|
29
|
+
/** The materialization payload carried on every delivery row. */
|
|
30
|
+
export interface DeliveryPayload {
|
|
31
|
+
actorId: string;
|
|
32
|
+
metadata: Record<string, unknown>;
|
|
33
|
+
occurrenceAt: number;
|
|
34
|
+
}
|
|
35
|
+
/** App-supplied, fully-formed delivery row (the app owns all id construction). */
|
|
36
|
+
export interface DeliveryRowInput {
|
|
37
|
+
deliveryId: string;
|
|
38
|
+
notificationType: string;
|
|
39
|
+
eventId: string;
|
|
40
|
+
/** `null` for shared-admin occurrences (the deliveryId hash uses the literal `'shared'`). */
|
|
41
|
+
recipientUid: string | null;
|
|
42
|
+
aggregationKey: string;
|
|
43
|
+
strategy: AggregationStrategy;
|
|
44
|
+
payload: DeliveryPayload;
|
|
45
|
+
payloadVersion: number;
|
|
46
|
+
materializationClass: MaterializationClass;
|
|
47
|
+
}
|
|
48
|
+
export type EnqueueRowResult = {
|
|
49
|
+
deliveryId: string;
|
|
50
|
+
outcome: 'created';
|
|
51
|
+
} | {
|
|
52
|
+
deliveryId: string;
|
|
53
|
+
outcome: 'duplicate';
|
|
54
|
+
} | {
|
|
55
|
+
deliveryId: string;
|
|
56
|
+
outcome: 'failed';
|
|
57
|
+
error: unknown;
|
|
58
|
+
};
|
|
59
|
+
export interface EnqueueResult {
|
|
60
|
+
results: EnqueueRowResult[];
|
|
61
|
+
/** True iff EVERY row is now represented by a created-or-pre-existing doc (page-cursor invariant input). */
|
|
62
|
+
allResolved: boolean;
|
|
63
|
+
}
|
|
64
|
+
export type MaterializeOutcome = 'materialized' | 'already-materialized' | 'missing' | 'skipped-non-queued';
|
|
65
|
+
export interface DeliveryLedger {
|
|
66
|
+
enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult>;
|
|
67
|
+
materialize(deliveryId: string): Promise<MaterializeOutcome>;
|
|
68
|
+
/** Materialize many rows with bounded concurrency; transient throws record a retry/dead-letter outcome. */
|
|
69
|
+
materializeMany(deliveryIds: string[], options?: {
|
|
70
|
+
concurrency?: number;
|
|
71
|
+
}): Promise<Record<MaterializeOutcome, number>>;
|
|
72
|
+
recordTransientFailure(deliveryId: string, error: unknown): Promise<void>;
|
|
73
|
+
deadLetter(deliveryId: string, error: unknown): Promise<void>;
|
|
74
|
+
replay(deliveryId: string): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Apply the aggregation strategy to produce the active-doc field delta.
|
|
78
|
+
* PURE — exported for direct unit testing. `existing` is the current active doc
|
|
79
|
+
* (or null to create). Always rotates `activityGeneration` and resets `seenAt`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function applyAggregation(params: {
|
|
82
|
+
strategy: AggregationStrategy;
|
|
83
|
+
existing: Pick<NotificationDoc, 'count' | 'latestActorIds'> | null;
|
|
84
|
+
actorId: string;
|
|
85
|
+
countCap: number;
|
|
86
|
+
actorCap: number;
|
|
87
|
+
buildMessage: (count: number) => string;
|
|
88
|
+
now: number;
|
|
89
|
+
generation: string;
|
|
90
|
+
}): {
|
|
91
|
+
count: number;
|
|
92
|
+
latestActorIds: string[];
|
|
93
|
+
message: string;
|
|
94
|
+
activityGeneration: string;
|
|
95
|
+
seenAt: number;
|
|
96
|
+
updatedAt: number;
|
|
97
|
+
};
|
|
98
|
+
export declare function createDeliveryLedger(db: ServerFirestore, config: NotificationSystemConfig): DeliveryLedger;
|
|
99
|
+
//# sourceMappingURL=delivery-ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-ledger.d.ts","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,299 @@
|
|
|
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 in NOTIFICATIONS_REDESIGN):
|
|
12
|
+
* - Enqueue = create-if-absent. `ALREADY_EXISTS` is a per-row duplicate no-op,
|
|
13
|
+
* never a failure — there is no marker-without-payload state, so the
|
|
14
|
+
* "marker created, payload lost" crash window cannot exist.
|
|
15
|
+
* - Materialize = ONE transaction that reads the delivery row + active card,
|
|
16
|
+
* applies the aggregation exactly once (per strategy + caps), rotates the
|
|
17
|
+
* opaque `activityGeneration`, resets `seenAt`, and flips the row to
|
|
18
|
+
* `materialized` — so the "applied but not marked" double-apply window is gone.
|
|
19
|
+
* Retrying a `materialized` row is a successful no-op.
|
|
20
|
+
* - TTL (`expireAt`, a real Firestore Timestamp via `config.timestampFromMillis`)
|
|
21
|
+
* is set ONLY at `materialized`; a `queued` or `deadLetter` row is NEVER TTL'd
|
|
22
|
+
* (round-19) so an unresolved delivery can't be silently deleted before replay.
|
|
23
|
+
*/
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
25
|
+
import { buildActiveNotificationDocId } from './activeNotificationId.js';
|
|
26
|
+
const DEFAULT_DELIVERIES_PATH = 'notificationDeliveries';
|
|
27
|
+
const DEFAULT_DELIVERY_TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 days
|
|
28
|
+
const DEFAULT_MAX_ATTEMPTS = 8;
|
|
29
|
+
const DEFAULT_COUNT_CAP = 5000;
|
|
30
|
+
const DEFAULT_ACTOR_CAP = 5;
|
|
31
|
+
const BACKOFF_BASE_MS = 60 * 1000; // 1 min
|
|
32
|
+
const BACKOFF_MAX_MS = 60 * 60 * 1000; // 1 hour
|
|
33
|
+
/** gRPC ALREADY_EXISTS (6) — the create-if-absent duplicate signal. */
|
|
34
|
+
function isAlreadyExists(error) {
|
|
35
|
+
const e = error;
|
|
36
|
+
return e?.code === 6 || /already.?exists/i.test(e?.message ?? '');
|
|
37
|
+
}
|
|
38
|
+
function backoffMs(attemptCount) {
|
|
39
|
+
const exp = Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, attemptCount - 1), BACKOFF_MAX_MS);
|
|
40
|
+
// Full jitter — spread retries so a burst of failures doesn't synchronize.
|
|
41
|
+
return Math.floor(exp / 2 + Math.random() * (exp / 2));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Apply the aggregation strategy to produce the active-doc field delta.
|
|
45
|
+
* PURE — exported for direct unit testing. `existing` is the current active doc
|
|
46
|
+
* (or null to create). Always rotates `activityGeneration` and resets `seenAt`.
|
|
47
|
+
*/
|
|
48
|
+
export function applyAggregation(params) {
|
|
49
|
+
const { strategy, existing, actorId, countCap, actorCap, buildMessage, now, generation } = params;
|
|
50
|
+
const prevActors = existing?.latestActorIds ?? [];
|
|
51
|
+
const latestActorIds = [actorId, ...prevActors.filter((id) => id !== actorId)].slice(0, actorCap);
|
|
52
|
+
// staticRelight never shows a count (stays 1); increment counts up to the cap.
|
|
53
|
+
const count = strategy === 'staticRelight'
|
|
54
|
+
? 1
|
|
55
|
+
: Math.min((existing?.count ?? 0) + 1, countCap);
|
|
56
|
+
return {
|
|
57
|
+
count,
|
|
58
|
+
latestActorIds,
|
|
59
|
+
message: buildMessage(count),
|
|
60
|
+
activityGeneration: generation,
|
|
61
|
+
seenAt: 0,
|
|
62
|
+
updatedAt: now,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function createDeliveryLedger(db, config) {
|
|
66
|
+
const deliveriesPath = config.deliveriesCollectionPath ?? DEFAULT_DELIVERIES_PATH;
|
|
67
|
+
const ttlMs = config.deliveryTtlMs ?? DEFAULT_DELIVERY_TTL_MS;
|
|
68
|
+
const maxAttempts = config.maxDeliveryAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
69
|
+
const configTimestampFromMillis = config.timestampFromMillis;
|
|
70
|
+
if (!configTimestampFromMillis) {
|
|
71
|
+
throw new Error('[notification-core] createDeliveryLedger requires config.timestampFromMillis (Firestore TTL needs a real Timestamp for expireAt).');
|
|
72
|
+
}
|
|
73
|
+
// Bind to an explicitly non-optional type so the nested transaction closures
|
|
74
|
+
// (called later) see it as defined, not `... | undefined`.
|
|
75
|
+
const toTimestamp = configTimestampFromMillis;
|
|
76
|
+
const deliveries = db.collection(deliveriesPath);
|
|
77
|
+
function deliveryRef(deliveryId) {
|
|
78
|
+
return deliveries.doc(deliveryId);
|
|
79
|
+
}
|
|
80
|
+
function buildDeliveryDoc(row, now) {
|
|
81
|
+
return {
|
|
82
|
+
deliveryId: row.deliveryId,
|
|
83
|
+
state: 'queued',
|
|
84
|
+
notificationType: row.notificationType,
|
|
85
|
+
eventId: row.eventId,
|
|
86
|
+
recipientUid: row.recipientUid,
|
|
87
|
+
aggregationKey: row.aggregationKey,
|
|
88
|
+
strategy: row.strategy,
|
|
89
|
+
payload: row.payload,
|
|
90
|
+
payloadVersion: row.payloadVersion,
|
|
91
|
+
materializationClass: row.materializationClass,
|
|
92
|
+
attemptCount: 0,
|
|
93
|
+
nextAttemptAt: now,
|
|
94
|
+
lastError: null,
|
|
95
|
+
createdAt: now,
|
|
96
|
+
materializedAt: null,
|
|
97
|
+
deadLetteredAt: null,
|
|
98
|
+
// No expireAt while queued (round-19).
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function getTypeConfig(type) {
|
|
102
|
+
const typeConfig = config.types[type];
|
|
103
|
+
if (!typeConfig)
|
|
104
|
+
throw new Error(`[notification-core] Unknown notification type: ${type}`);
|
|
105
|
+
const categoryConfig = config.categories[typeConfig.category];
|
|
106
|
+
if (!categoryConfig)
|
|
107
|
+
throw new Error(`[notification-core] Unknown category: ${typeConfig.category}`);
|
|
108
|
+
return { typeConfig, categoryConfig };
|
|
109
|
+
}
|
|
110
|
+
async function enqueue(rows) {
|
|
111
|
+
if (rows.length === 0)
|
|
112
|
+
return { results: [], allResolved: true };
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
const settled = await Promise.allSettled(rows.map((row) => deliveryRef(row.deliveryId)
|
|
115
|
+
.create(buildDeliveryDoc(row, now))
|
|
116
|
+
.then(() => ({ deliveryId: row.deliveryId, outcome: 'created' }))));
|
|
117
|
+
const results = settled.map((s, i) => {
|
|
118
|
+
const deliveryId = rows[i].deliveryId;
|
|
119
|
+
if (s.status === 'fulfilled')
|
|
120
|
+
return s.value;
|
|
121
|
+
if (isAlreadyExists(s.reason))
|
|
122
|
+
return { deliveryId, outcome: 'duplicate' };
|
|
123
|
+
return { deliveryId, outcome: 'failed', error: s.reason };
|
|
124
|
+
});
|
|
125
|
+
return { results, allResolved: results.every((r) => r.outcome !== 'failed') };
|
|
126
|
+
}
|
|
127
|
+
async function materialize(deliveryId) {
|
|
128
|
+
return db.runTransaction(async (tx) => {
|
|
129
|
+
const dRef = deliveryRef(deliveryId);
|
|
130
|
+
const dSnap = await tx.get(dRef);
|
|
131
|
+
if (!dSnap.exists)
|
|
132
|
+
return 'missing';
|
|
133
|
+
const d = (dSnap.data() ?? {});
|
|
134
|
+
const state = d.state;
|
|
135
|
+
if (state === 'materialized')
|
|
136
|
+
return 'already-materialized';
|
|
137
|
+
if (state !== 'queued')
|
|
138
|
+
return 'skipped-non-queued';
|
|
139
|
+
const notificationType = d.notificationType;
|
|
140
|
+
const aggregationKey = d.aggregationKey;
|
|
141
|
+
const recipientUid = d.recipientUid ?? null;
|
|
142
|
+
const strategy = d.strategy ?? 'increment';
|
|
143
|
+
const payload = d.payload ?? { actorId: '', metadata: {}, occurrenceAt: Date.now() };
|
|
144
|
+
const { typeConfig, categoryConfig } = getTypeConfig(notificationType);
|
|
145
|
+
const activeRef = db.collection(categoryConfig.activePath).doc(buildActiveNotificationDocId({
|
|
146
|
+
category: typeConfig.category,
|
|
147
|
+
audienceType: categoryConfig.audienceType,
|
|
148
|
+
targetUserId: recipientUid,
|
|
149
|
+
dedupKey: aggregationKey,
|
|
150
|
+
notificationType,
|
|
151
|
+
}));
|
|
152
|
+
const activeSnap = await tx.get(activeRef); // all reads before writes
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const generation = randomUUID();
|
|
155
|
+
const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;
|
|
156
|
+
const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;
|
|
157
|
+
const buildMessage = (count) => typeConfig.messagePattern(payload.metadata, count);
|
|
158
|
+
if (activeSnap.exists) {
|
|
159
|
+
const existing = (activeSnap.data() ?? {});
|
|
160
|
+
const delta = applyAggregation({
|
|
161
|
+
strategy,
|
|
162
|
+
existing,
|
|
163
|
+
actorId: payload.actorId,
|
|
164
|
+
countCap,
|
|
165
|
+
actorCap,
|
|
166
|
+
buildMessage,
|
|
167
|
+
now,
|
|
168
|
+
generation,
|
|
169
|
+
});
|
|
170
|
+
tx.update(activeRef, { ...delta });
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const delta = applyAggregation({
|
|
174
|
+
strategy,
|
|
175
|
+
existing: null,
|
|
176
|
+
actorId: payload.actorId,
|
|
177
|
+
countCap,
|
|
178
|
+
actorCap,
|
|
179
|
+
buildMessage,
|
|
180
|
+
now,
|
|
181
|
+
generation,
|
|
182
|
+
});
|
|
183
|
+
const targetPath = typeof typeConfig.defaultTargetPath === 'function'
|
|
184
|
+
? typeConfig.defaultTargetPath(payload.metadata)
|
|
185
|
+
: typeConfig.defaultTargetPath;
|
|
186
|
+
const newDoc = {
|
|
187
|
+
type: notificationType,
|
|
188
|
+
dedupKey: aggregationKey,
|
|
189
|
+
category: typeConfig.category,
|
|
190
|
+
targetUserId: recipientUid,
|
|
191
|
+
title: typeConfig.titlePattern(payload.metadata),
|
|
192
|
+
message: delta.message,
|
|
193
|
+
count: delta.count,
|
|
194
|
+
latestActorIds: delta.latestActorIds,
|
|
195
|
+
targetPath,
|
|
196
|
+
metadata: payload.metadata,
|
|
197
|
+
seenAt: 0,
|
|
198
|
+
activityGeneration: generation,
|
|
199
|
+
createdAt: now,
|
|
200
|
+
updatedAt: now,
|
|
201
|
+
};
|
|
202
|
+
tx.set(activeRef, newDoc);
|
|
203
|
+
}
|
|
204
|
+
tx.update(dRef, {
|
|
205
|
+
state: 'materialized',
|
|
206
|
+
materializedAt: now,
|
|
207
|
+
expireAt: toTimestamp(now + ttlMs),
|
|
208
|
+
});
|
|
209
|
+
return 'materialized';
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function recordTransientFailure(deliveryId, error) {
|
|
213
|
+
await db.runTransaction(async (tx) => {
|
|
214
|
+
const dRef = deliveryRef(deliveryId);
|
|
215
|
+
const dSnap = await tx.get(dRef);
|
|
216
|
+
if (!dSnap.exists)
|
|
217
|
+
return;
|
|
218
|
+
const d = (dSnap.data() ?? {});
|
|
219
|
+
if (d.state !== 'queued')
|
|
220
|
+
return;
|
|
221
|
+
const attemptCount = (d.attemptCount ?? 0) + 1;
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
224
|
+
if (attemptCount >= maxAttempts) {
|
|
225
|
+
tx.update(dRef, {
|
|
226
|
+
state: 'deadLetter',
|
|
227
|
+
attemptCount,
|
|
228
|
+
lastError: message,
|
|
229
|
+
deadLetteredAt: now,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
tx.update(dRef, { attemptCount, nextAttemptAt: now + backoffMs(attemptCount), lastError: message });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async function deadLetter(deliveryId, error) {
|
|
238
|
+
await db.runTransaction(async (tx) => {
|
|
239
|
+
const dRef = deliveryRef(deliveryId);
|
|
240
|
+
const dSnap = await tx.get(dRef);
|
|
241
|
+
if (!dSnap.exists)
|
|
242
|
+
return;
|
|
243
|
+
const d = (dSnap.data() ?? {});
|
|
244
|
+
if (d.state !== 'queued')
|
|
245
|
+
return;
|
|
246
|
+
tx.update(dRef, {
|
|
247
|
+
state: 'deadLetter',
|
|
248
|
+
attemptCount: (d.attemptCount ?? 0) + 1,
|
|
249
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
250
|
+
deadLetteredAt: Date.now(),
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async function replay(deliveryId) {
|
|
255
|
+
await db.runTransaction(async (tx) => {
|
|
256
|
+
const dRef = deliveryRef(deliveryId);
|
|
257
|
+
const dSnap = await tx.get(dRef);
|
|
258
|
+
if (!dSnap.exists)
|
|
259
|
+
return;
|
|
260
|
+
const d = (dSnap.data() ?? {});
|
|
261
|
+
if (d.state !== 'deadLetter')
|
|
262
|
+
return;
|
|
263
|
+
tx.update(dRef, {
|
|
264
|
+
state: 'queued',
|
|
265
|
+
attemptCount: 0,
|
|
266
|
+
nextAttemptAt: Date.now(),
|
|
267
|
+
lastError: null,
|
|
268
|
+
deadLetteredAt: null,
|
|
269
|
+
expireAt: null, // clear any TTL set on a prior terminal (round-19)
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
async function materializeMany(deliveryIds, options) {
|
|
274
|
+
const concurrency = Math.max(1, options?.concurrency ?? 10);
|
|
275
|
+
const tally = {
|
|
276
|
+
materialized: 0,
|
|
277
|
+
'already-materialized': 0,
|
|
278
|
+
missing: 0,
|
|
279
|
+
'skipped-non-queued': 0,
|
|
280
|
+
};
|
|
281
|
+
let cursor = 0;
|
|
282
|
+
async function worker() {
|
|
283
|
+
while (cursor < deliveryIds.length) {
|
|
284
|
+
const id = deliveryIds[cursor++];
|
|
285
|
+
try {
|
|
286
|
+
const outcome = await materialize(id);
|
|
287
|
+
tally[outcome] += 1;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
await recordTransientFailure(id, error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, deliveryIds.length) }, worker));
|
|
295
|
+
return tally;
|
|
296
|
+
}
|
|
297
|
+
return { enqueue, materialize, materializeMany, recordTransientFailure, deadLetter, replay };
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=delivery-ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-ledger.js","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"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"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -4,5 +4,9 @@ export { archiveNotificationHelper, archiveAllNotificationsHelper, } from './arc
|
|
|
4
4
|
export { markSeenHelper } from './markSeenHelper.js';
|
|
5
5
|
export { NotificationPermissionError } from './errors.js';
|
|
6
6
|
export { buildActiveNotificationDocId } from './activeNotificationId.js';
|
|
7
|
+
export { createDeliveryLedger, applyAggregation } from './delivery-ledger.js';
|
|
8
|
+
export type { DeliveryLedger, DeliveryRowInput, DeliveryPayload, DeliveryState, AggregationStrategy, MaterializationClass, EnqueueResult, EnqueueRowResult, MaterializeOutcome, } from './delivery-ledger.js';
|
|
9
|
+
export { markNotificationSeenWithGeneration, archiveNotificationWithGeneration, } from './observed-generation.js';
|
|
10
|
+
export type { MarkSeenOutcome, ArchiveOutcome } from './observed-generation.js';
|
|
7
11
|
export type { ServerFirestore, ServerCollectionRef, ServerQuery, ServerQuerySnapshot, ServerDocSnapshot, ServerDocRef, ServerWriteBatch, ServerTransaction, CreateNotificationInput, NotificationHelper, } from './types.js';
|
|
8
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAGzE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,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,mBAAmB,EACnB,oBAAoB,EACpB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,GACnB,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,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -5,4 +5,8 @@ export { archiveNotificationHelper, archiveAllNotificationsHelper, } from './arc
|
|
|
5
5
|
export { markSeenHelper } from './markSeenHelper.js';
|
|
6
6
|
export { NotificationPermissionError } from './errors.js';
|
|
7
7
|
export { buildActiveNotificationDocId } from './activeNotificationId.js';
|
|
8
|
+
// Delivery ledger (notification redesign — P1)
|
|
9
|
+
export { createDeliveryLedger, applyAggregation } from './delivery-ledger.js';
|
|
10
|
+
// Observed-generation seen/archive protocol (notification redesign — P1)
|
|
11
|
+
export { markNotificationSeenWithGeneration, archiveNotificationWithGeneration, } from './observed-generation.js';
|
|
8
12
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAEzE,+CAA+C;AAC/C,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAa9E,yEAAyE;AACzE,OAAO,EACL,kCAAkC,EAClC,iCAAiC,GAClC,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
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 (ttt-core) and passes the refs in; this module owns
|
|
13
|
+
* only the transactional state machine, not the id formulas.
|
|
14
|
+
*/
|
|
15
|
+
import type { ServerFirestore, ServerDocRef } from './types.js';
|
|
16
|
+
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
|
+
export declare function markNotificationSeenWithGeneration(db: ServerFirestore, activeRef: ServerDocRef, params: {
|
|
23
|
+
observedActivityGeneration: string;
|
|
24
|
+
now?: number;
|
|
25
|
+
}): Promise<MarkSeenOutcome>;
|
|
26
|
+
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
|
+
export declare function archiveNotificationWithGeneration(db: ServerFirestore, params: {
|
|
36
|
+
activeRef: ServerDocRef;
|
|
37
|
+
/** Deterministic history doc id = hash('notification-archive', category, audienceScope, requestId) (app-built). */
|
|
38
|
+
historyRef: ServerDocRef;
|
|
39
|
+
requestId: string;
|
|
40
|
+
observedActivityGeneration: string;
|
|
41
|
+
/** Deterministic payload hash (app-built) — the replay-vs-conflict discriminator. */
|
|
42
|
+
payloadHash: string;
|
|
43
|
+
category: string;
|
|
44
|
+
audienceScope: string;
|
|
45
|
+
/** Native-TTL expiry for the history doc; converted to a Timestamp via the factory. */
|
|
46
|
+
expireAtMs: number;
|
|
47
|
+
timestampFromMillis: (ms: number) => unknown;
|
|
48
|
+
/** Admin history quick-access field (admin category only). */
|
|
49
|
+
handledBy?: string;
|
|
50
|
+
}): Promise<ArchiveOutcome>;
|
|
51
|
+
//# sourceMappingURL=observed-generation.d.ts.map
|
|
@@ -0,0 +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,MAAM,YAAY,CAAC;AAEhE,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;CACpB,GACA,OAAO,CAAC,cAAc,CAAC,CA+CzB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
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 (ttt-core) and passes the refs in; this module owns
|
|
13
|
+
* 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
|
+
export async function markNotificationSeenWithGeneration(db, activeRef, params) {
|
|
21
|
+
return db.runTransaction(async (tx) => {
|
|
22
|
+
const snap = await tx.get(activeRef);
|
|
23
|
+
if (!snap.exists)
|
|
24
|
+
return 'missing';
|
|
25
|
+
const data = (snap.data() ?? {});
|
|
26
|
+
if (data.activityGeneration !== params.observedActivityGeneration) {
|
|
27
|
+
return 'generation-mismatch';
|
|
28
|
+
}
|
|
29
|
+
tx.update(activeRef, {
|
|
30
|
+
seenAt: params.now ?? Date.now(),
|
|
31
|
+
seenAtGeneration: params.observedActivityGeneration,
|
|
32
|
+
});
|
|
33
|
+
return 'seen';
|
|
34
|
+
});
|
|
35
|
+
}
|
|
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
|
+
export async function archiveNotificationWithGeneration(db, params) {
|
|
45
|
+
const { activeRef, historyRef, requestId, observedActivityGeneration, payloadHash, category, audienceScope, expireAtMs, timestampFromMillis, handledBy, } = params;
|
|
46
|
+
return db.runTransaction(async (tx) => {
|
|
47
|
+
// Reads first (Firestore reads-before-writes).
|
|
48
|
+
const historySnap = await tx.get(historyRef);
|
|
49
|
+
if (historySnap.exists) {
|
|
50
|
+
const existing = (historySnap.data() ?? {});
|
|
51
|
+
return existing.payloadHash === payloadHash ? 'replayed' : 'conflict';
|
|
52
|
+
}
|
|
53
|
+
const activeSnap = await tx.get(activeRef);
|
|
54
|
+
if (!activeSnap.exists)
|
|
55
|
+
return 'missing';
|
|
56
|
+
const activeData = (activeSnap.data() ?? {});
|
|
57
|
+
if (activeData.activityGeneration !== observedActivityGeneration) {
|
|
58
|
+
return 'generation-mismatch';
|
|
59
|
+
}
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const historyDoc = {
|
|
62
|
+
archiveOccurrenceId: historyRef.id,
|
|
63
|
+
requestId,
|
|
64
|
+
payloadHash,
|
|
65
|
+
activeId: activeRef.id,
|
|
66
|
+
observedActivityGeneration,
|
|
67
|
+
category,
|
|
68
|
+
audienceScope,
|
|
69
|
+
archivedSnapshot: activeData,
|
|
70
|
+
archivedAt: now,
|
|
71
|
+
expireAt: timestampFromMillis(expireAtMs),
|
|
72
|
+
};
|
|
73
|
+
if (handledBy !== undefined)
|
|
74
|
+
historyDoc.handledBy = handledBy;
|
|
75
|
+
tx.set(historyRef, historyDoc);
|
|
76
|
+
tx.delete(activeRef);
|
|
77
|
+
return 'archived';
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=observed-generation.js.map
|
|
@@ -0,0 +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,MAeC;IAED,MAAM,EACJ,SAAS,EACT,UAAU,EACV,SAAS,EACT,0BAA0B,EAC1B,WAAW,EACX,QAAQ,EACR,aAAa,EACb,UAAU,EACV,mBAAmB,EACnB,SAAS,GACV,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,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/server/types.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface ServerDocRef {
|
|
|
50
50
|
merge?: boolean;
|
|
51
51
|
}): Promise<unknown>;
|
|
52
52
|
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
|
+
create(data: Record<string, unknown>): Promise<unknown>;
|
|
53
55
|
delete(): Promise<unknown>;
|
|
54
56
|
get(): Promise<ServerDocSnapshot>;
|
|
55
57
|
}
|
|
@@ -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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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -44,6 +44,17 @@ export interface NotificationDoc {
|
|
|
44
44
|
* uniformity but their unread indicator is existence-based and ignores it.
|
|
45
45
|
*/
|
|
46
46
|
seenAt: number;
|
|
47
|
+
/**
|
|
48
|
+
* Opaque per-generation token (uuid), minted FRESH whenever the active doc is
|
|
49
|
+
* created or materially re-lit. The SEEN/ARCHIVE precondition compares against
|
|
50
|
+
* THIS, not a restartable integer: the active doc id is deterministic, so a
|
|
51
|
+
* delete+recreate would restart an integer and let a stale tab's "version 1"
|
|
52
|
+
* match a different card it never saw (ABA). An opaque token cannot repeat.
|
|
53
|
+
* Optional on the legacy active doc shape; the ledger materializer always sets it.
|
|
54
|
+
*/
|
|
55
|
+
activityGeneration?: string;
|
|
56
|
+
/** The `activityGeneration` observed at the time `seenAt` was last set. */
|
|
57
|
+
seenAtGeneration?: string;
|
|
47
58
|
/** First occurrence */
|
|
48
59
|
createdAt: number;
|
|
49
60
|
/** Latest occurrence */
|
|
@@ -138,6 +149,20 @@ export interface NotificationSystemConfig {
|
|
|
138
149
|
batchIntervalMinutes?: number;
|
|
139
150
|
/** Firestore collection for pending notifications (default 'pendingNotifications') */
|
|
140
151
|
pendingCollectionPath?: string;
|
|
152
|
+
/** Firestore collection for the delivery ledger (default 'notificationDeliveries'). */
|
|
153
|
+
deliveriesCollectionPath?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Factory that converts epoch-ms to the app's Firestore `Timestamp` instance.
|
|
156
|
+
* REQUIRED for the delivery ledger: Firestore native TTL acts ONLY on
|
|
157
|
+
* `Timestamp` fields, so `expireAt` must be a real Timestamp (never a number).
|
|
158
|
+
* The generic package never imports firebase-admin, so the app injects this
|
|
159
|
+
* (e.g. `(ms) => admin.firestore.Timestamp.fromMillis(ms)`).
|
|
160
|
+
*/
|
|
161
|
+
timestampFromMillis?: (ms: number) => unknown;
|
|
162
|
+
/** Delivery-row TTL after materialization, in ms (default 90 days). */
|
|
163
|
+
deliveryTtlMs?: number;
|
|
164
|
+
/** Max delivery attempts before a row dead-letters (default 8). */
|
|
165
|
+
maxDeliveryAttempts?: number;
|
|
141
166
|
}
|
|
142
167
|
export interface UseActiveNotificationsOptions {
|
|
143
168
|
config: NotificationSystemConfig;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,uBAAuB;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IAGjB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAG5B,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,OAAO,EAAE,MAAM,CAAC;IAGhB,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;IAGzB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAGlC;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/notification-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Shared notification system for TTT Productions apps — active/history two-tier architecture with dedup, batch processing, and themed UI components",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@tanstack/react-query": ">=5.0.0",
|
|
42
|
-
"@ttt-productions/query-core": "^0.
|
|
43
|
-
"@ttt-productions/ui-core": "^0.
|
|
42
|
+
"@ttt-productions/query-core": "^0.13.2",
|
|
43
|
+
"@ttt-productions/ui-core": "^0.11.0",
|
|
44
44
|
"firebase": ">=12.0.0",
|
|
45
45
|
"react": ">=19.0.0",
|
|
46
46
|
"react-dom": ">=19.0.0"
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@tanstack/react-query": "^5.0.0",
|
|
70
|
-
"@ttt-productions/query-core": "^0.
|
|
71
|
-
"@ttt-productions/ui-core": "^0.
|
|
70
|
+
"@ttt-productions/query-core": "^0.13.2",
|
|
71
|
+
"@ttt-productions/ui-core": "^0.11.0",
|
|
72
72
|
"@types/react": "^19.0.0",
|
|
73
73
|
"@types/react-dom": "^19.0.0",
|
|
74
74
|
"firebase": "^11.0.0",
|