@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.
- package/README.md +9 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/react/components/NotificationEmptyState.d.ts +0 -3
- package/dist/react/components/NotificationEmptyState.d.ts.map +1 -1
- package/dist/react/components/NotificationEmptyState.js +0 -3
- package/dist/react/components/NotificationEmptyState.js.map +1 -1
- package/dist/react/components/NotificationHistoryList.d.ts +0 -6
- package/dist/react/components/NotificationHistoryList.d.ts.map +1 -1
- package/dist/react/components/NotificationHistoryList.js +0 -8
- package/dist/react/components/NotificationHistoryList.js.map +1 -1
- package/dist/react/components/NotificationList.d.ts +0 -3
- package/dist/react/components/NotificationList.d.ts.map +1 -1
- package/dist/react/components/NotificationList.js +2 -29
- package/dist/react/components/NotificationList.js.map +1 -1
- package/dist/react/components/NotificationUnreadBadge.d.ts +0 -5
- package/dist/react/components/NotificationUnreadBadge.d.ts.map +1 -1
- package/dist/react/components/NotificationUnreadBadge.js +0 -5
- package/dist/react/components/NotificationUnreadBadge.js.map +1 -1
- package/dist/react/components/relative-time.d.ts +0 -3
- package/dist/react/components/relative-time.d.ts.map +1 -1
- package/dist/react/components/relative-time.js +0 -3
- package/dist/react/components/relative-time.js.map +1 -1
- package/dist/react/hooks/useActiveNotifications.d.ts +0 -4
- package/dist/react/hooks/useActiveNotifications.d.ts.map +1 -1
- package/dist/react/hooks/useActiveNotifications.js +2 -6
- package/dist/react/hooks/useActiveNotifications.js.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.d.ts +0 -30
- package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.js +1 -43
- package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
- package/dist/react/hooks/useArchiveNotification.d.ts +0 -8
- package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveNotification.js +0 -8
- package/dist/react/hooks/useArchiveNotification.js.map +1 -1
- package/dist/react/hooks/useNotificationHistory.d.ts +0 -14
- package/dist/react/hooks/useNotificationHistory.d.ts.map +1 -1
- package/dist/react/hooks/useNotificationHistory.js +2 -18
- package/dist/react/hooks/useNotificationHistory.js.map +1 -1
- package/dist/react/hooks/useUnreadCount.d.ts +0 -22
- package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
- package/dist/react/hooks/useUnreadCount.js +1 -22
- package/dist/react/hooks/useUnreadCount.js.map +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +0 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react/types.d.ts +0 -42
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/types.js.map +1 -1
- package/dist/schemas.d.ts +0 -22
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +0 -25
- package/dist/schemas.js.map +1 -1
- package/dist/server/activeNotificationId.d.ts +1 -22
- package/dist/server/activeNotificationId.d.ts.map +1 -1
- package/dist/server/activeNotificationId.js +0 -16
- package/dist/server/activeNotificationId.js.map +1 -1
- package/dist/server/delivery-ledger.d.ts +1 -73
- package/dist/server/delivery-ledger.d.ts.map +1 -1
- package/dist/server/delivery-ledger.js +7 -59
- package/dist/server/delivery-ledger.js.map +1 -1
- package/dist/server/errors.d.ts +0 -9
- package/dist/server/errors.d.ts.map +1 -1
- package/dist/server/errors.js +1 -11
- package/dist/server/errors.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/observed-generation.d.ts +1 -41
- package/dist/server/observed-generation.d.ts.map +1 -1
- package/dist/server/observed-generation.js +1 -31
- package/dist/server/observed-generation.js.map +1 -1
- package/dist/server/processBatchHelper.d.ts +0 -19
- package/dist/server/processBatchHelper.d.ts.map +1 -1
- package/dist/server/processBatchHelper.js +2 -46
- package/dist/server/processBatchHelper.js.map +1 -1
- package/dist/server/types.d.ts +0 -9
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +0 -4
- package/dist/server/types.js.map +1 -1
- package/dist/types.d.ts +2 -196
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +1 -1
- package/package.json +5 -5
- package/src/styles/notifications.css +0 -14
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Observed-generation seen/archive protocol (notification redesign — P1).
|
|
3
|
-
*
|
|
4
|
-
* The SEEN/ARCHIVE precondition compares the card's current opaque
|
|
5
|
-
* `activityGeneration` against the generation the client actually observed when
|
|
6
|
-
* it rendered the row — so activity arriving after render (or a delete+recreate
|
|
7
|
-
* that rotated the token) is never silently swallowed. Archive additionally
|
|
8
|
-
* carries a retry-stable `requestId`; the deterministic history doc id makes a
|
|
9
|
-
* replay return the stored result and touch nothing.
|
|
10
|
-
*
|
|
11
|
-
* GENERIC: the app constructs the deterministic ids (`historyRef`, `payloadHash`)
|
|
12
|
-
* via its own canonical hash (in the consuming app's core package) and passes the
|
|
13
|
-
* refs in; this module owns only the transactional state machine, not the id formulas.
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Set `seenAt`/`seenAtGeneration` ONLY if the card's current `activityGeneration`
|
|
17
|
-
* still matches the observed one; otherwise no-op (the row stays unseen so newer
|
|
18
|
-
* activity is never swallowed).
|
|
19
|
-
*/
|
|
20
1
|
export async function markNotificationSeenWithGeneration(db, activeRef, params) {
|
|
21
2
|
return db.runTransaction(async (tx) => {
|
|
22
3
|
const snap = await tx.get(activeRef);
|
|
@@ -33,18 +14,10 @@ export async function markNotificationSeenWithGeneration(db, activeRef, params)
|
|
|
33
14
|
return 'seen';
|
|
34
15
|
});
|
|
35
16
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Archive one active card into history, idempotently and replay-safely.
|
|
38
|
-
*
|
|
39
|
-
* - history doc already exists + same `payloadHash` ⇒ `replayed` (touch NOTHING).
|
|
40
|
-
* - history doc already exists + DIFFERENT `payloadHash` ⇒ `conflict` (caller alerts).
|
|
41
|
-
* - first-seen: archive only if the active card's `activityGeneration` still
|
|
42
|
-
* matches the observed generation; else `generation-mismatch` (leave it active).
|
|
43
|
-
*/
|
|
44
17
|
export async function archiveNotificationWithGeneration(db, params) {
|
|
45
18
|
const { activeRef, historyRef, requestId, observedActivityGeneration, payloadHash, category, audienceScope, expireAtMs, timestampFromMillis, handledBy, auditWrite, } = params;
|
|
46
19
|
return db.runTransaction(async (tx) => {
|
|
47
|
-
//
|
|
20
|
+
// Firestore transactions take every read before any write.
|
|
48
21
|
const historySnap = await tx.get(historyRef);
|
|
49
22
|
if (historySnap.exists) {
|
|
50
23
|
const existing = (historySnap.data() ?? {});
|
|
@@ -74,9 +47,6 @@ export async function archiveNotificationWithGeneration(db, params) {
|
|
|
74
47
|
historyDoc.handledBy = handledBy;
|
|
75
48
|
tx.set(historyRef, historyDoc);
|
|
76
49
|
tx.delete(activeRef);
|
|
77
|
-
// Compose any caller-supplied write (e.g. an audit event) into THIS
|
|
78
|
-
// transaction so it commits atomically with the archive. Only reached on the
|
|
79
|
-
// successful path, after all reads, so reads-before-writes is preserved.
|
|
80
50
|
await auditWrite?.(tx);
|
|
81
51
|
return 'archived';
|
|
82
52
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observed-generation.js","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"observed-generation.js","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,EAAmB,EACnB,SAAuB,EACvB,MAA4D;IAE5D,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACnC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QAC5D,IAAK,IAAI,CAAC,kBAAyC,KAAK,MAAM,CAAC,0BAA0B,EAAE,CAAC;YAC1F,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QACD,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE;YACnB,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;YAChC,gBAAgB,EAAE,MAAM,CAAC,0BAA0B;SACpD,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC;AAKD,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,EAAmB,EACnB,MAaC;IAED,MAAM,EACJ,SAAS,EACT,UAAU,EACV,SAAS,EACT,0BAA0B,EAC1B,WAAW,EACX,QAAQ,EACR,aAAa,EACb,UAAU,EACV,mBAAmB,EACnB,SAAS,EACT,UAAU,GACX,GAAG,MAAM,CAAC;IAEX,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,2DAA2D;QAC3D,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;YACvE,OAAQ,QAAQ,CAAC,WAAkC,KAAK,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;QAChG,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACzC,MAAM,UAAU,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QACxE,IAAK,UAAU,CAAC,kBAAyC,KAAK,0BAA0B,EAAE,CAAC;YACzF,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,UAAU,GAA4B;YAC1C,mBAAmB,EAAE,UAAU,CAAC,EAAE;YAClC,SAAS;YACT,WAAW;YACX,QAAQ,EAAE,SAAS,CAAC,EAAE;YACtB,0BAA0B;YAC1B,QAAQ;YACR,aAAa;YACb,gBAAgB,EAAE,UAAU;YAC5B,UAAU,EAAE,GAAG;YACf,QAAQ,EAAE,mBAAmB,CAAC,UAAU,CAAC;SAC1C,CAAC;QACF,IAAI,SAAS,KAAK,SAAS;YAAE,UAAU,CAAC,SAAS,GAAG,SAAS,CAAC;QAE9D,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC/B,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACrB,MAAM,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QACvB,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ServerFirestore, ServerDocRef, ServerTransaction } from './types.js';\n\nexport type MarkSeenOutcome = 'seen' | 'generation-mismatch' | 'missing';\n\nexport async function markNotificationSeenWithGeneration(\n db: ServerFirestore,\n activeRef: ServerDocRef,\n params: { observedActivityGeneration: string; now?: number },\n): Promise<MarkSeenOutcome> {\n return db.runTransaction(async (tx) => {\n const snap = await tx.get(activeRef);\n if (!snap.exists) return 'missing';\n const data = (snap.data() ?? {}) as Record<string, unknown>;\n if ((data.activityGeneration as string | undefined) !== params.observedActivityGeneration) {\n return 'generation-mismatch';\n }\n tx.update(activeRef, {\n seenAt: params.now ?? Date.now(),\n seenAtGeneration: params.observedActivityGeneration,\n });\n return 'seen';\n });\n}\n\nexport type ArchiveOutcome =\n 'archived' | 'replayed' | 'conflict' | 'generation-mismatch' | 'missing';\n\nexport async function archiveNotificationWithGeneration(\n db: ServerFirestore,\n params: {\n activeRef: ServerDocRef;\n historyRef: ServerDocRef;\n requestId: string;\n observedActivityGeneration: string;\n payloadHash: string;\n category: string;\n audienceScope: string;\n expireAtMs: number;\n timestampFromMillis: (ms: number) => unknown;\n handledBy?: string;\n /** Runs in the write phase, so it must not read (Firestore reads-before-writes). */\n auditWrite?: (txn: ServerTransaction) => void | Promise<void>;\n },\n): Promise<ArchiveOutcome> {\n const {\n activeRef,\n historyRef,\n requestId,\n observedActivityGeneration,\n payloadHash,\n category,\n audienceScope,\n expireAtMs,\n timestampFromMillis,\n handledBy,\n auditWrite,\n } = params;\n\n return db.runTransaction(async (tx) => {\n // Firestore transactions take every read before any write.\n const historySnap = await tx.get(historyRef);\n if (historySnap.exists) {\n const existing = (historySnap.data() ?? {}) as Record<string, unknown>;\n return (existing.payloadHash as string | undefined) === payloadHash ? 'replayed' : 'conflict';\n }\n const activeSnap = await tx.get(activeRef);\n if (!activeSnap.exists) return 'missing';\n const activeData = (activeSnap.data() ?? {}) as Record<string, unknown>;\n if ((activeData.activityGeneration as string | undefined) !== observedActivityGeneration) {\n return 'generation-mismatch';\n }\n\n const now = Date.now();\n const historyDoc: Record<string, unknown> = {\n archiveOccurrenceId: historyRef.id,\n requestId,\n payloadHash,\n activeId: activeRef.id,\n observedActivityGeneration,\n category,\n audienceScope,\n archivedSnapshot: activeData,\n archivedAt: now,\n expireAt: timestampFromMillis(expireAtMs),\n };\n if (handledBy !== undefined) historyDoc.handledBy = handledBy;\n\n tx.set(historyRef, historyDoc);\n tx.delete(activeRef);\n await auditWrite?.(tx);\n return 'archived';\n });\n}\n"]}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Batch processor helper — called by the app's scheduled Cloud Function.
|
|
3
|
-
* Reads pending notifications, groups by dedupKey, creates/increments active docs.
|
|
4
|
-
*/
|
|
5
1
|
import type { NotificationSystemConfig } from '../types.js';
|
|
6
2
|
import type { ServerFirestore } from './types.js';
|
|
7
3
|
interface BatchProcessResult {
|
|
@@ -9,21 +5,6 @@ interface BatchProcessResult {
|
|
|
9
5
|
notificationsCreated: number;
|
|
10
6
|
notificationsUpdated: number;
|
|
11
7
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Process the pending notifications queue.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* // In your scheduled Cloud Function:
|
|
18
|
-
* export const processNotificationBatch = onSchedule(
|
|
19
|
-
* { schedule: 'every 10 minutes' },
|
|
20
|
-
* async () => {
|
|
21
|
-
* const result = await processBatchHelper(db as any, TTT_NOTIFICATION_CONFIG);
|
|
22
|
-
* logger.info('Batch processed', result);
|
|
23
|
-
* }
|
|
24
|
-
* );
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
8
|
export declare function processBatchHelper(db: ServerFirestore, config: NotificationSystemConfig): Promise<BatchProcessResult>;
|
|
28
9
|
export {};
|
|
29
10
|
//# sourceMappingURL=processBatchHelper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processBatchHelper.d.ts","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"processBatchHelper.d.ts","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAuB,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAQlD,UAAU,kBAAkB;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CAyK7B"}
|
|
@@ -1,30 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Batch processor helper — called by the app's scheduled Cloud Function.
|
|
3
|
-
* Reads pending notifications, groups by dedupKey, creates/increments active docs.
|
|
4
|
-
*/
|
|
5
1
|
import { buildActiveNotificationDocId } from './activeNotificationId.js';
|
|
6
2
|
const DEFAULT_COUNT_CAP = 5000;
|
|
7
3
|
const DEFAULT_ACTOR_CAP = 5;
|
|
8
4
|
const PROCESSING_BATCH_SIZE = 500;
|
|
9
5
|
const MAX_ITERATIONS = 20;
|
|
10
|
-
/**
|
|
11
|
-
* Process the pending notifications queue.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* // In your scheduled Cloud Function:
|
|
16
|
-
* export const processNotificationBatch = onSchedule(
|
|
17
|
-
* { schedule: 'every 10 minutes' },
|
|
18
|
-
* async () => {
|
|
19
|
-
* const result = await processBatchHelper(db as any, TTT_NOTIFICATION_CONFIG);
|
|
20
|
-
* logger.info('Batch processed', result);
|
|
21
|
-
* }
|
|
22
|
-
* );
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
6
|
export async function processBatchHelper(db, config) {
|
|
26
7
|
const pendingPath = config.pendingCollectionPath ?? 'pendingNotifications';
|
|
27
|
-
const cutoff = Date.now() - 30_000;
|
|
8
|
+
const cutoff = Date.now() - 30_000;
|
|
28
9
|
let totalProcessed = 0;
|
|
29
10
|
let notificationsCreated = 0;
|
|
30
11
|
let notificationsUpdated = 0;
|
|
@@ -41,7 +22,6 @@ export async function processBatchHelper(db, config) {
|
|
|
41
22
|
if (snapshot.empty) {
|
|
42
23
|
break;
|
|
43
24
|
}
|
|
44
|
-
// Group by dedupKey
|
|
45
25
|
const groups = new Map();
|
|
46
26
|
for (const docSnap of snapshot.docs) {
|
|
47
27
|
const data = docSnap.data();
|
|
@@ -52,17 +32,12 @@ export async function processBatchHelper(db, config) {
|
|
|
52
32
|
if (!categoryConfig)
|
|
53
33
|
continue;
|
|
54
34
|
const dedupKey = typeConfig.dedupKeyPattern(data.metadata);
|
|
55
|
-
// Personal notifications dedup per-recipient: a multi-recipient fan-out
|
|
56
|
-
// (same type/metadata, different targetUserId) must never collapse into
|
|
57
|
-
// one recipient's active doc. Shared notifications are intentionally a
|
|
58
|
-
// single doc for all admins (targetUserId is null).
|
|
59
35
|
const groupKey = categoryConfig.audienceType === 'personal'
|
|
60
36
|
? `${data.category}::${data.targetUserId}::${dedupKey}`
|
|
61
37
|
: `${data.category}::${dedupKey}`;
|
|
62
38
|
const existing = groups.get(groupKey);
|
|
63
39
|
if (existing) {
|
|
64
40
|
existing.count++;
|
|
65
|
-
// Add actor id if not already present
|
|
66
41
|
if (!existing.actorIds.includes(data.actorId)) {
|
|
67
42
|
existing.actorIds.push(data.actorId);
|
|
68
43
|
}
|
|
@@ -80,16 +55,6 @@ export async function processBatchHelper(db, config) {
|
|
|
80
55
|
});
|
|
81
56
|
}
|
|
82
57
|
}
|
|
83
|
-
// Process each group: create or increment the group's active doc.
|
|
84
|
-
// Each group's groupKey uniquely determines a single active doc — the doc ID
|
|
85
|
-
// is a deterministic hash of exactly the dedupKey + category (+ targetUserId
|
|
86
|
-
// for personal) that compose the groupKey — so two distinct groups can never
|
|
87
|
-
// resolve to the same active doc and there is no cross-group contention. The
|
|
88
|
-
// work per group is independent I/O, so Promise.all turns N sequential
|
|
89
|
-
// round-trips into one parallel wave, removing the timeout risk under high
|
|
90
|
-
// dedup-group counts. WITHIN a dedup scope, the transactional read-modify-write
|
|
91
|
-
// on the deterministic ID makes concurrent writers (a racing real-time send,
|
|
92
|
-
// or an overlapping batch run) converge on one doc with correct counts.
|
|
93
58
|
const groupResults = await Promise.all(Array.from(groups.values()).map(async (group) => {
|
|
94
59
|
const typeConfig = config.types[group.type];
|
|
95
60
|
if (!typeConfig)
|
|
@@ -101,9 +66,6 @@ export async function processBatchHelper(db, config) {
|
|
|
101
66
|
const dedupKey = typeConfig.dedupKeyPattern(group.metadata);
|
|
102
67
|
const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;
|
|
103
68
|
const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;
|
|
104
|
-
// Personal notifications are scoped to the recipient so different
|
|
105
|
-
// recipients never share an active doc; shared notifications omit
|
|
106
|
-
// targetUserId by design.
|
|
107
69
|
const activeDocRef = db.collection(activePath).doc(buildActiveNotificationDocId({
|
|
108
70
|
category: group.category,
|
|
109
71
|
audienceType: categoryConfig.audienceType,
|
|
@@ -113,11 +75,9 @@ export async function processBatchHelper(db, config) {
|
|
|
113
75
|
return db.runTransaction(async (tx) => {
|
|
114
76
|
const existingSnap = await tx.get(activeDocRef);
|
|
115
77
|
if (existingSnap.exists) {
|
|
116
|
-
// Increment existing
|
|
117
78
|
const existingData = (existingSnap.data() ?? {});
|
|
118
79
|
const currentCount = existingData.count || 1;
|
|
119
80
|
const currentActorIds = existingData.latestActorIds || [];
|
|
120
|
-
// Merge actor ids (new first, deduped, capped)
|
|
121
81
|
const newActorIds = group.actorIds.filter((id) => !currentActorIds.includes(id));
|
|
122
82
|
const mergedActorIds = [...newActorIds, ...currentActorIds].slice(0, actorCap);
|
|
123
83
|
const newCount = Math.min(currentCount + group.count, countCap);
|
|
@@ -125,15 +85,13 @@ export async function processBatchHelper(db, config) {
|
|
|
125
85
|
count: newCount,
|
|
126
86
|
latestActorIds: mergedActorIds,
|
|
127
87
|
message: typeConfig.messagePattern(group.metadata, newCount),
|
|
128
|
-
// New activity on an existing notification re-lights the unread badge.
|
|
129
88
|
seenAt: 0,
|
|
130
89
|
updatedAt: Date.now(),
|
|
131
90
|
});
|
|
132
91
|
return 'updated';
|
|
133
92
|
}
|
|
134
93
|
else {
|
|
135
|
-
//
|
|
136
|
-
// linkless — OMIT the field (Firestore rejects `undefined` values).
|
|
94
|
+
// Firestore rejects `undefined` values, so a linkless type omits the field.
|
|
137
95
|
const targetPath = typeConfig.defaultTargetPath === undefined
|
|
138
96
|
? undefined
|
|
139
97
|
: typeof typeConfig.defaultTargetPath === 'function'
|
|
@@ -151,7 +109,6 @@ export async function processBatchHelper(db, config) {
|
|
|
151
109
|
latestActorIds: group.actorIds.slice(0, actorCap),
|
|
152
110
|
...(targetPath === undefined ? {} : { targetPath }),
|
|
153
111
|
metadata: group.metadata,
|
|
154
|
-
// Active docs are created unseen so the unread count() predicate matches.
|
|
155
112
|
seenAt: 0,
|
|
156
113
|
createdAt: now,
|
|
157
114
|
updatedAt: now,
|
|
@@ -167,7 +124,6 @@ export async function processBatchHelper(db, config) {
|
|
|
167
124
|
else if (r === 'updated')
|
|
168
125
|
notificationsUpdated++;
|
|
169
126
|
}
|
|
170
|
-
// Delete processed pending docs in batches of 500
|
|
171
127
|
const allDocs = snapshot.docs;
|
|
172
128
|
for (let i = 0; i < allDocs.length; i += 500) {
|
|
173
129
|
const chunk = allDocs.slice(i, i + 500);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processBatchHelper.js","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Batch processor helper — called by the app's scheduled Cloud Function.\n * Reads pending notifications, groups by dedupKey, creates/increments active docs.\n */\n\nimport type { NotificationSystemConfig, PendingNotification } from '../types.js';\nimport type { ServerFirestore } from './types.js';\nimport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\nconst DEFAULT_COUNT_CAP = 5000;\nconst DEFAULT_ACTOR_CAP = 5;\nconst PROCESSING_BATCH_SIZE = 500;\nconst MAX_ITERATIONS = 20;\n\ninterface BatchProcessResult {\n totalProcessed: number;\n notificationsCreated: number;\n notificationsUpdated: number;\n}\n\n/**\n * Process the pending notifications queue.\n *\n * @example\n * ```ts\n * // In your scheduled Cloud Function:\n * export const processNotificationBatch = onSchedule(\n * { schedule: 'every 10 minutes' },\n * async () => {\n * const result = await processBatchHelper(db as any, TTT_NOTIFICATION_CONFIG);\n * logger.info('Batch processed', result);\n * }\n * );\n * ```\n */\nexport async function processBatchHelper(\n db: ServerFirestore,\n config: NotificationSystemConfig,\n): Promise<BatchProcessResult> {\n const pendingPath = config.pendingCollectionPath ?? 'pendingNotifications';\n const cutoff = Date.now() - 30_000; // Process items older than 30 seconds\n\n let totalProcessed = 0;\n let notificationsCreated = 0;\n let notificationsUpdated = 0;\n let iterations = 0;\n let hasMore = true;\n\n while (hasMore && iterations < MAX_ITERATIONS) {\n iterations++;\n\n const pendingQuery = db\n .collection(pendingPath)\n .where('createdAt', '<=', cutoff)\n .orderBy('createdAt', 'asc')\n .limit(PROCESSING_BATCH_SIZE);\n\n const snapshot = await pendingQuery.get();\n\n if (snapshot.empty) {\n break;\n }\n\n // Group by dedupKey\n const groups = new Map<\n string,\n {\n type: string;\n category: string;\n targetUserId: string | null;\n metadata: Record<string, unknown>;\n actorIds: string[];\n count: number;\n docs: typeof snapshot.docs;\n }\n >();\n\n for (const docSnap of snapshot.docs) {\n const data = docSnap.data() as Omit<PendingNotification, 'id'>;\n const typeConfig = config.types[data.type];\n if (!typeConfig) continue;\n const categoryConfig = config.categories[data.category];\n if (!categoryConfig) continue;\n\n const dedupKey = typeConfig.dedupKeyPattern(data.metadata);\n // Personal notifications dedup per-recipient: a multi-recipient fan-out\n // (same type/metadata, different targetUserId) must never collapse into\n // one recipient's active doc. Shared notifications are intentionally a\n // single doc for all admins (targetUserId is null).\n const groupKey =\n categoryConfig.audienceType === 'personal'\n ? `${data.category}::${data.targetUserId}::${dedupKey}`\n : `${data.category}::${dedupKey}`;\n\n const existing = groups.get(groupKey);\n if (existing) {\n existing.count++;\n // Add actor id if not already present\n if (!existing.actorIds.includes(data.actorId)) {\n existing.actorIds.push(data.actorId);\n }\n existing.docs.push(docSnap);\n } else {\n groups.set(groupKey, {\n type: data.type,\n category: data.category,\n targetUserId: data.targetUserId,\n metadata: data.metadata,\n actorIds: [data.actorId],\n count: 1,\n docs: [docSnap],\n });\n }\n }\n\n // Process each group: create or increment the group's active doc.\n // Each group's groupKey uniquely determines a single active doc — the doc ID\n // is a deterministic hash of exactly the dedupKey + category (+ targetUserId\n // for personal) that compose the groupKey — so two distinct groups can never\n // resolve to the same active doc and there is no cross-group contention. The\n // work per group is independent I/O, so Promise.all turns N sequential\n // round-trips into one parallel wave, removing the timeout risk under high\n // dedup-group counts. WITHIN a dedup scope, the transactional read-modify-write\n // on the deterministic ID makes concurrent writers (a racing real-time send,\n // or an overlapping batch run) converge on one doc with correct counts.\n const groupResults = await Promise.all(\n Array.from(groups.values()).map(async (group): Promise<'created' | 'updated' | 'skipped'> => {\n const typeConfig = config.types[group.type];\n if (!typeConfig) return 'skipped';\n\n const categoryConfig = config.categories[group.category];\n if (!categoryConfig) return 'skipped';\n\n const activePath = categoryConfig.activePath;\n const dedupKey = typeConfig.dedupKeyPattern(group.metadata);\n const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;\n const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;\n\n // Personal notifications are scoped to the recipient so different\n // recipients never share an active doc; shared notifications omit\n // targetUserId by design.\n const activeDocRef = db.collection(activePath).doc(\n buildActiveNotificationDocId({\n category: group.category,\n audienceType: categoryConfig.audienceType,\n targetUserId: group.targetUserId,\n dedupKey,\n }),\n );\n\n return db.runTransaction(async (tx): Promise<'created' | 'updated'> => {\n const existingSnap = await tx.get(activeDocRef);\n\n if (existingSnap.exists) {\n // Increment existing\n const existingData = (existingSnap.data() ?? {}) as Record<string, unknown>;\n const currentCount = (existingData.count as number) || 1;\n const currentActorIds = (existingData.latestActorIds as string[]) || [];\n\n // Merge actor ids (new first, deduped, capped)\n const newActorIds = group.actorIds.filter((id) => !currentActorIds.includes(id));\n const mergedActorIds = [...newActorIds, ...currentActorIds].slice(0, actorCap);\n\n const newCount = Math.min(currentCount + group.count, countCap);\n\n tx.update(activeDocRef, {\n count: newCount,\n latestActorIds: mergedActorIds,\n message: typeConfig.messagePattern(group.metadata, newCount),\n // New activity on an existing notification re-lights the unread badge.\n seenAt: 0,\n updatedAt: Date.now(),\n });\n\n return 'updated';\n } else {\n // Create new notification. A type with no defaultTargetPath is\n // linkless — OMIT the field (Firestore rejects `undefined` values).\n const targetPath =\n typeConfig.defaultTargetPath === undefined\n ? undefined\n : typeof typeConfig.defaultTargetPath === 'function'\n ? typeConfig.defaultTargetPath(group.metadata)\n : typeConfig.defaultTargetPath;\n const now = Date.now();\n\n const newDoc = {\n type: group.type,\n dedupKey,\n category: group.category,\n targetUserId: group.targetUserId,\n title: typeConfig.titlePattern(group.metadata),\n message: typeConfig.messagePattern(group.metadata, group.count),\n count: group.count,\n latestActorIds: group.actorIds.slice(0, actorCap),\n ...(targetPath === undefined ? {} : { targetPath }),\n metadata: group.metadata,\n // Active docs are created unseen so the unread count() predicate matches.\n seenAt: 0,\n createdAt: now,\n updatedAt: now,\n };\n\n tx.set(activeDocRef, newDoc);\n return 'created';\n }\n });\n }),\n );\n\n for (const r of groupResults) {\n if (r === 'created') notificationsCreated++;\n else if (r === 'updated') notificationsUpdated++;\n }\n\n // Delete processed pending docs in batches of 500\n const allDocs = snapshot.docs;\n for (let i = 0; i < allDocs.length; i += 500) {\n const chunk = allDocs.slice(i, i + 500);\n const batch = db.batch();\n chunk.forEach((docSnap) => batch.delete(docSnap.ref));\n await batch.commit();\n }\n\n totalProcessed += snapshot.size;\n\n if (snapshot.size < PROCESSING_BATCH_SIZE) {\n hasMore = false;\n }\n }\n\n return { totalProcessed, notificationsCreated, notificationsUpdated };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"processBatchHelper.js","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"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","sourcesContent":["import type { NotificationSystemConfig, PendingNotification } from '../types.js';\nimport type { ServerFirestore } from './types.js';\nimport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\nconst DEFAULT_COUNT_CAP = 5000;\nconst DEFAULT_ACTOR_CAP = 5;\nconst PROCESSING_BATCH_SIZE = 500;\nconst MAX_ITERATIONS = 20;\n\ninterface BatchProcessResult {\n totalProcessed: number;\n notificationsCreated: number;\n notificationsUpdated: number;\n}\n\nexport async function processBatchHelper(\n db: ServerFirestore,\n config: NotificationSystemConfig,\n): Promise<BatchProcessResult> {\n const pendingPath = config.pendingCollectionPath ?? 'pendingNotifications';\n const cutoff = Date.now() - 30_000;\n\n let totalProcessed = 0;\n let notificationsCreated = 0;\n let notificationsUpdated = 0;\n let iterations = 0;\n let hasMore = true;\n\n while (hasMore && iterations < MAX_ITERATIONS) {\n iterations++;\n\n const pendingQuery = db\n .collection(pendingPath)\n .where('createdAt', '<=', cutoff)\n .orderBy('createdAt', 'asc')\n .limit(PROCESSING_BATCH_SIZE);\n\n const snapshot = await pendingQuery.get();\n\n if (snapshot.empty) {\n break;\n }\n\n const groups = new Map<\n string,\n {\n type: string;\n category: string;\n targetUserId: string | null;\n metadata: Record<string, unknown>;\n actorIds: string[];\n count: number;\n docs: typeof snapshot.docs;\n }\n >();\n\n for (const docSnap of snapshot.docs) {\n const data = docSnap.data() as Omit<PendingNotification, 'id'>;\n const typeConfig = config.types[data.type];\n if (!typeConfig) continue;\n const categoryConfig = config.categories[data.category];\n if (!categoryConfig) continue;\n\n const dedupKey = typeConfig.dedupKeyPattern(data.metadata);\n const groupKey =\n categoryConfig.audienceType === 'personal'\n ? `${data.category}::${data.targetUserId}::${dedupKey}`\n : `${data.category}::${dedupKey}`;\n\n const existing = groups.get(groupKey);\n if (existing) {\n existing.count++;\n if (!existing.actorIds.includes(data.actorId)) {\n existing.actorIds.push(data.actorId);\n }\n existing.docs.push(docSnap);\n } else {\n groups.set(groupKey, {\n type: data.type,\n category: data.category,\n targetUserId: data.targetUserId,\n metadata: data.metadata,\n actorIds: [data.actorId],\n count: 1,\n docs: [docSnap],\n });\n }\n }\n\n const groupResults = await Promise.all(\n Array.from(groups.values()).map(async (group): Promise<'created' | 'updated' | 'skipped'> => {\n const typeConfig = config.types[group.type];\n if (!typeConfig) return 'skipped';\n\n const categoryConfig = config.categories[group.category];\n if (!categoryConfig) return 'skipped';\n\n const activePath = categoryConfig.activePath;\n const dedupKey = typeConfig.dedupKeyPattern(group.metadata);\n const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;\n const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;\n\n const activeDocRef = db.collection(activePath).doc(\n buildActiveNotificationDocId({\n category: group.category,\n audienceType: categoryConfig.audienceType,\n targetUserId: group.targetUserId,\n dedupKey,\n }),\n );\n\n return db.runTransaction(async (tx): Promise<'created' | 'updated'> => {\n const existingSnap = await tx.get(activeDocRef);\n\n if (existingSnap.exists) {\n const existingData = (existingSnap.data() ?? {}) as Record<string, unknown>;\n const currentCount = (existingData.count as number) || 1;\n const currentActorIds = (existingData.latestActorIds as string[]) || [];\n\n const newActorIds = group.actorIds.filter((id) => !currentActorIds.includes(id));\n const mergedActorIds = [...newActorIds, ...currentActorIds].slice(0, actorCap);\n\n const newCount = Math.min(currentCount + group.count, countCap);\n\n tx.update(activeDocRef, {\n count: newCount,\n latestActorIds: mergedActorIds,\n message: typeConfig.messagePattern(group.metadata, newCount),\n seenAt: 0,\n updatedAt: Date.now(),\n });\n\n return 'updated';\n } else {\n // Firestore rejects `undefined` values, so a linkless type omits the field.\n const targetPath =\n typeConfig.defaultTargetPath === undefined\n ? undefined\n : typeof typeConfig.defaultTargetPath === 'function'\n ? typeConfig.defaultTargetPath(group.metadata)\n : typeConfig.defaultTargetPath;\n const now = Date.now();\n\n const newDoc = {\n type: group.type,\n dedupKey,\n category: group.category,\n targetUserId: group.targetUserId,\n title: typeConfig.titlePattern(group.metadata),\n message: typeConfig.messagePattern(group.metadata, group.count),\n count: group.count,\n latestActorIds: group.actorIds.slice(0, actorCap),\n ...(targetPath === undefined ? {} : { targetPath }),\n metadata: group.metadata,\n seenAt: 0,\n createdAt: now,\n updatedAt: now,\n };\n\n tx.set(activeDocRef, newDoc);\n return 'created';\n }\n });\n }),\n );\n\n for (const r of groupResults) {\n if (r === 'created') notificationsCreated++;\n else if (r === 'updated') notificationsUpdated++;\n }\n\n const allDocs = snapshot.docs;\n for (let i = 0; i < allDocs.length; i += 500) {\n const chunk = allDocs.slice(i, i + 500);\n const batch = db.batch();\n chunk.forEach((docSnap) => batch.delete(docSnap.ref));\n await batch.commit();\n }\n\n totalProcessed += snapshot.size;\n\n if (snapshot.size < PROCESSING_BATCH_SIZE) {\n hasMore = false;\n }\n }\n\n return { totalProcessed, notificationsCreated, notificationsUpdated };\n}\n"]}
|
package/dist/server/types.d.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-side types for notification-core backend helpers.
|
|
3
|
-
* These use firebase-admin Firestore types passed in by the calling app.
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Firestore interface — matches firebase-admin's Firestore type.
|
|
7
|
-
* We accept this instead of importing firebase-admin directly.
|
|
8
|
-
*/
|
|
9
1
|
export interface ServerFirestore {
|
|
10
2
|
collection(path: string): ServerCollectionRef;
|
|
11
3
|
doc(path: string): ServerDocRef;
|
|
@@ -50,7 +42,6 @@ export interface ServerDocRef {
|
|
|
50
42
|
merge?: boolean;
|
|
51
43
|
}): Promise<unknown>;
|
|
52
44
|
update(data: Record<string, unknown>): Promise<unknown>;
|
|
53
|
-
/** Create-if-absent. Rejects with an `already-exists` error (gRPC code 6) if the doc exists. */
|
|
54
45
|
create(data: Record<string, unknown>): Promise<unknown>;
|
|
55
46
|
delete(): Promise<unknown>;
|
|
56
47
|
get(): Promise<ServerDocSnapshot>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAAC;IAC9C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;IAChC,KAAK,IAAI,gBAAgB,CAAC;IAC1B,cAAc,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC/F;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACnD,GAAG,CACD,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,iBAAiB,CAAC;IACrB,MAAM,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CAAC;IAC5E,MAAM,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAAC;CAC9C;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAC/B,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,WAAW,CAAC;IAC9D,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,WAAW,CAAC;IAChE,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9B,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,WAAW,CAAC;IAC9D,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,WAAW,CAAC;IAChE,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9B,GAAG,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,iBAAiB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC5C,GAAG,EAAE,YAAY,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpF,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3B,GAAG,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CACD,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,gBAAgB,CAAC;IACpB,MAAM,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,CAAC;IAC3E,MAAM,CAAC,GAAG,EAAE,YAAY,GAAG,gBAAgB,CAAC;IAC5C,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5B"}
|
package/dist/server/types.js
CHANGED
package/dist/server/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface ServerFirestore {\n collection(path: string): ServerCollectionRef;\n doc(path: string): ServerDocRef;\n batch(): ServerWriteBatch;\n runTransaction<T>(updateFunction: (transaction: ServerTransaction) => Promise<T>): Promise<T>;\n}\n\nexport interface ServerTransaction {\n get(ref: ServerDocRef): Promise<ServerDocSnapshot>;\n set(\n ref: ServerDocRef,\n data: Record<string, unknown>,\n options?: { merge?: boolean },\n ): ServerTransaction;\n update(ref: ServerDocRef, data: Record<string, unknown>): ServerTransaction;\n delete(ref: ServerDocRef): ServerTransaction;\n}\n\nexport interface ServerCollectionRef {\n doc(id?: string): ServerDocRef;\n where(field: string, op: string, value: unknown): ServerQuery;\n orderBy(field: string, direction?: 'asc' | 'desc'): ServerQuery;\n limit(n: number): ServerQuery;\n add(data: Record<string, unknown>): Promise<ServerDocRef>;\n}\n\nexport interface ServerQuery {\n where(field: string, op: string, value: unknown): ServerQuery;\n orderBy(field: string, direction?: 'asc' | 'desc'): ServerQuery;\n limit(n: number): ServerQuery;\n get(): Promise<ServerQuerySnapshot>;\n}\n\nexport interface ServerQuerySnapshot {\n empty: boolean;\n size: number;\n docs: ServerDocSnapshot[];\n}\n\nexport interface ServerDocSnapshot {\n id: string;\n exists: boolean;\n data(): Record<string, unknown> | undefined;\n ref: ServerDocRef;\n}\n\nexport interface ServerDocRef {\n id: string;\n set(data: Record<string, unknown>, options?: { merge?: boolean }): Promise<unknown>;\n update(data: Record<string, unknown>): Promise<unknown>;\n create(data: Record<string, unknown>): Promise<unknown>;\n delete(): Promise<unknown>;\n get(): Promise<ServerDocSnapshot>;\n}\n\nexport interface ServerWriteBatch {\n set(\n ref: ServerDocRef,\n data: Record<string, unknown>,\n options?: { merge?: boolean },\n ): ServerWriteBatch;\n update(ref: ServerDocRef, data: Record<string, unknown>): ServerWriteBatch;\n delete(ref: ServerDocRef): ServerWriteBatch;\n commit(): Promise<unknown>;\n}\n"]}
|