@ttt-productions/notification-core 0.4.1 → 0.4.3
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 +30 -138
- package/dist/react/components/NotificationList.d.ts +1 -1
- package/dist/react/components/NotificationList.d.ts.map +1 -1
- package/dist/react/components/NotificationList.js +7 -20
- package/dist/react/components/NotificationList.js.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.d.ts +10 -12
- package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.js +10 -58
- package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
- package/dist/react/hooks/useArchiveNotification.d.ts +12 -11
- package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveNotification.js +12 -39
- package/dist/react/hooks/useArchiveNotification.js.map +1 -1
- package/dist/react/hooks/useUnreadCount.d.ts +30 -21
- package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
- package/dist/react/hooks/useUnreadCount.js +29 -15
- package/dist/react/hooks/useUnreadCount.js.map +1 -1
- package/dist/server/archiveNotificationHelper.d.ts +14 -3
- package/dist/server/archiveNotificationHelper.d.ts.map +1 -1
- package/dist/server/archiveNotificationHelper.js +35 -7
- package/dist/server/archiveNotificationHelper.js.map +1 -1
- package/dist/server/createNotificationHelper.d.ts +1 -1
- package/dist/server/createNotificationHelper.d.ts.map +1 -1
- package/dist/server/createNotificationHelper.js +35 -13
- package/dist/server/createNotificationHelper.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/markSeenHelper.d.ts +17 -0
- package/dist/server/markSeenHelper.d.ts.map +1 -0
- package/dist/server/markSeenHelper.js +29 -0
- package/dist/server/markSeenHelper.js.map +1 -0
- package/dist/server/processBatchHelper.d.ts.map +1 -1
- package/dist/server/processBatchHelper.js +12 -15
- package/dist/server/processBatchHelper.js.map +1 -1
- package/dist/server/types.d.ts +7 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/types.d.ts +45 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +73 -73
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UseUnreadCountOptions } from '../../types.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Unread-count badge backed by a Firestore `count()` aggregation (a server-side
|
|
4
|
+
* count, not a doc fetch). Polls for cost control; displays capped
|
|
5
|
+
* (`hasMore` → `99+`).
|
|
6
|
+
*
|
|
7
|
+
* - **personal** categories count **unseen** active items (`seenAt == 0`),
|
|
8
|
+
* scoped to the caller (`targetUserId == uid`);
|
|
9
|
+
* - **shared** categories have no per-admin seen state, so the indicator is
|
|
10
|
+
* existence-based — a count of all active items, with no `seenAt` predicate.
|
|
11
|
+
*
|
|
12
|
+
* The personal `seenAt == 0` predicate needs a composite index, captured as an
|
|
13
|
+
* app-side index step.
|
|
5
14
|
*
|
|
6
15
|
* @example
|
|
7
16
|
* ```tsx
|
|
@@ -35,9 +44,9 @@ export declare function useUnreadCount({ config, userId, category, enabled, refe
|
|
|
35
44
|
isRefetching: boolean;
|
|
36
45
|
isStale: boolean;
|
|
37
46
|
isEnabled: boolean;
|
|
38
|
-
refetch: (options?: import("@tanstack/query
|
|
39
|
-
fetchStatus: import("@tanstack/query
|
|
40
|
-
promise: Promise<
|
|
47
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<number, Error>>;
|
|
48
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
49
|
+
promise: Promise<number>;
|
|
41
50
|
count: number;
|
|
42
51
|
hasMore: boolean;
|
|
43
52
|
} | {
|
|
@@ -63,9 +72,9 @@ export declare function useUnreadCount({ config, userId, category, enabled, refe
|
|
|
63
72
|
isRefetching: boolean;
|
|
64
73
|
isStale: boolean;
|
|
65
74
|
isEnabled: boolean;
|
|
66
|
-
refetch: (options?: import("@tanstack/query
|
|
67
|
-
fetchStatus: import("@tanstack/query
|
|
68
|
-
promise: Promise<
|
|
75
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<number, Error>>;
|
|
76
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
77
|
+
promise: Promise<number>;
|
|
69
78
|
count: number;
|
|
70
79
|
hasMore: boolean;
|
|
71
80
|
} | {
|
|
@@ -91,9 +100,9 @@ export declare function useUnreadCount({ config, userId, category, enabled, refe
|
|
|
91
100
|
isRefetching: boolean;
|
|
92
101
|
isStale: boolean;
|
|
93
102
|
isEnabled: boolean;
|
|
94
|
-
refetch: (options?: import("@tanstack/query
|
|
95
|
-
fetchStatus: import("@tanstack/query
|
|
96
|
-
promise: Promise<
|
|
103
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<number, Error>>;
|
|
104
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
105
|
+
promise: Promise<number>;
|
|
97
106
|
count: number;
|
|
98
107
|
hasMore: boolean;
|
|
99
108
|
} | {
|
|
@@ -119,9 +128,9 @@ export declare function useUnreadCount({ config, userId, category, enabled, refe
|
|
|
119
128
|
isRefetching: boolean;
|
|
120
129
|
isStale: boolean;
|
|
121
130
|
isEnabled: boolean;
|
|
122
|
-
refetch: (options?: import("@tanstack/query
|
|
123
|
-
fetchStatus: import("@tanstack/query
|
|
124
|
-
promise: Promise<
|
|
131
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<number, Error>>;
|
|
132
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
133
|
+
promise: Promise<number>;
|
|
125
134
|
count: number;
|
|
126
135
|
hasMore: boolean;
|
|
127
136
|
} | {
|
|
@@ -147,9 +156,9 @@ export declare function useUnreadCount({ config, userId, category, enabled, refe
|
|
|
147
156
|
isRefetching: boolean;
|
|
148
157
|
isStale: boolean;
|
|
149
158
|
isEnabled: boolean;
|
|
150
|
-
refetch: (options?: import("@tanstack/query
|
|
151
|
-
fetchStatus: import("@tanstack/query
|
|
152
|
-
promise: Promise<
|
|
159
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<number, Error>>;
|
|
160
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
161
|
+
promise: Promise<number>;
|
|
153
162
|
count: number;
|
|
154
163
|
hasMore: boolean;
|
|
155
164
|
} | {
|
|
@@ -175,9 +184,9 @@ export declare function useUnreadCount({ config, userId, category, enabled, refe
|
|
|
175
184
|
isRefetching: boolean;
|
|
176
185
|
isStale: boolean;
|
|
177
186
|
isEnabled: boolean;
|
|
178
|
-
refetch: (options?: import("@tanstack/query
|
|
179
|
-
fetchStatus: import("@tanstack/query
|
|
180
|
-
promise: Promise<
|
|
187
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<number, Error>>;
|
|
188
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
189
|
+
promise: Promise<number>;
|
|
181
190
|
count: number;
|
|
182
191
|
hasMore: boolean;
|
|
183
192
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useUnreadCount.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useUnreadCount.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useUnreadCount.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useUnreadCount.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAK5D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,eAA0C,EAC1C,UAAgC,GACjC,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkCvB"}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
3
|
-
import { where,
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { collection, query, where, getCountFromServer, } from 'firebase/firestore';
|
|
4
|
+
import { useFirestoreDb } from '@ttt-productions/query-core/react';
|
|
4
5
|
const DEFAULT_REFETCH_INTERVAL = 30_000;
|
|
5
6
|
const DEFAULT_COUNT_LIMIT = 99;
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* Unread-count badge backed by a Firestore `count()` aggregation (a server-side
|
|
9
|
+
* count, not a doc fetch). Polls for cost control; displays capped
|
|
10
|
+
* (`hasMore` → `99+`).
|
|
11
|
+
*
|
|
12
|
+
* - **personal** categories count **unseen** active items (`seenAt == 0`),
|
|
13
|
+
* scoped to the caller (`targetUserId == uid`);
|
|
14
|
+
* - **shared** categories have no per-admin seen state, so the indicator is
|
|
15
|
+
* existence-based — a count of all active items, with no `seenAt` predicate.
|
|
16
|
+
*
|
|
17
|
+
* The personal `seenAt == 0` predicate needs a composite index, captured as an
|
|
18
|
+
* app-side index step.
|
|
9
19
|
*
|
|
10
20
|
* @example
|
|
11
21
|
* ```tsx
|
|
@@ -17,28 +27,32 @@ const DEFAULT_COUNT_LIMIT = 99;
|
|
|
17
27
|
* ```
|
|
18
28
|
*/
|
|
19
29
|
export function useUnreadCount({ config, userId, category, enabled = true, refetchInterval = DEFAULT_REFETCH_INTERVAL, countLimit = DEFAULT_COUNT_LIMIT, }) {
|
|
30
|
+
const db = useFirestoreDb();
|
|
20
31
|
const categoryConfig = config.categories[category];
|
|
21
32
|
if (!categoryConfig) {
|
|
22
33
|
throw new Error(`[notification-core] Unknown category: ${category}`);
|
|
23
34
|
}
|
|
24
35
|
const collectionPath = categoryConfig.activePath;
|
|
25
36
|
const isPersonal = categoryConfig.audienceType === 'personal';
|
|
26
|
-
const
|
|
27
|
-
...(isPersonal ? [where('targetUserId', '==', userId)] : []),
|
|
28
|
-
orderBy('updatedAt', 'desc'),
|
|
29
|
-
limit(countLimit),
|
|
30
|
-
];
|
|
31
|
-
const { data: notifications, ...rest } = useFirestoreCollection({
|
|
32
|
-
collectionPath,
|
|
37
|
+
const { data, ...rest } = useQuery({
|
|
33
38
|
queryKey: ['notifications', 'unread-count', category, userId],
|
|
34
|
-
|
|
39
|
+
queryFn: async () => {
|
|
40
|
+
const collectionRef = collection(db, collectionPath);
|
|
41
|
+
const constraints = isPersonal
|
|
42
|
+
? [where('targetUserId', '==', userId), where('seenAt', '==', 0)]
|
|
43
|
+
: [];
|
|
44
|
+
const q = constraints.length > 0 ? query(collectionRef, ...constraints) : collectionRef;
|
|
45
|
+
const snapshot = await getCountFromServer(q);
|
|
46
|
+
return snapshot.data().count;
|
|
47
|
+
},
|
|
35
48
|
enabled: enabled && !!userId,
|
|
36
|
-
subscribe: false,
|
|
37
49
|
staleTime: refetchInterval,
|
|
50
|
+
refetchInterval,
|
|
38
51
|
});
|
|
52
|
+
const count = data ?? 0;
|
|
39
53
|
return {
|
|
40
|
-
count
|
|
41
|
-
hasMore:
|
|
54
|
+
count,
|
|
55
|
+
hasMore: count > countLimit,
|
|
42
56
|
...rest,
|
|
43
57
|
};
|
|
44
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useUnreadCount.js","sourceRoot":"","sources":["../../../src/react/hooks/useUnreadCount.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"useUnreadCount.js","sourceRoot":"","sources":["../../../src/react/hooks/useUnreadCount.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EACL,UAAU,EACV,KAAK,EACL,KAAK,EACL,kBAAkB,GAEnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGnE,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,cAAc,CAAC,EAC7B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,eAAe,GAAG,wBAAwB,EAC1C,UAAU,GAAG,mBAAmB,GACV;IACtB,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;IAE5B,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,yCAAyC,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,UAAU,CAAC;IACjD,MAAM,UAAU,GAAG,cAAc,CAAC,YAAY,KAAK,UAAU,CAAC;IAE9D,MAAM,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC;QACjC,QAAQ,EAAE,CAAC,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC;QAC7D,OAAO,EAAE,KAAK,IAAqB,EAAE;YACnC,MAAM,aAAa,GAAG,UAAU,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC;YACrD,MAAM,WAAW,GAAsB,UAAU;gBAC/C,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;gBACjE,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;YACxF,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,CAAC,CAAC,CAAC;YAC7C,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;QAC/B,CAAC;QACD,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,SAAS,EAAE,eAAe;QAC1B,eAAe;KAChB,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC;IAExB,OAAO;QACL,KAAK;QACL,OAAO,EAAE,KAAK,GAAG,UAAU;QAC3B,GAAG,IAAI;KACR,CAAC;AACJ,CAAC"}
|
|
@@ -7,20 +7,31 @@ import type { ServerFirestore } from './types.js';
|
|
|
7
7
|
interface ArchiveInput {
|
|
8
8
|
notificationId: string;
|
|
9
9
|
category: string;
|
|
10
|
-
|
|
10
|
+
/** uid of the caller requesting the archive (ownership is verified against it). */
|
|
11
|
+
callerUid: string;
|
|
12
|
+
/** Whether the caller holds an admin/jrAdmin claim (required for shared notifications). */
|
|
13
|
+
callerIsAdmin: boolean;
|
|
11
14
|
archivalInfo: ArchivalInfo;
|
|
15
|
+
/**
|
|
16
|
+
* App-supplied epoch ms persisted on the history doc to back native TTL.
|
|
17
|
+
* The retention window is app policy; the helper just writes the value.
|
|
18
|
+
*/
|
|
19
|
+
expireAt?: number;
|
|
12
20
|
}
|
|
13
21
|
interface ArchiveAllInput {
|
|
14
22
|
category: string;
|
|
15
|
-
|
|
23
|
+
callerUid: string;
|
|
24
|
+
callerIsAdmin: boolean;
|
|
16
25
|
archivalInfo: ArchivalInfo;
|
|
26
|
+
expireAt?: number;
|
|
17
27
|
}
|
|
18
28
|
interface ArchiveResult {
|
|
19
29
|
success: boolean;
|
|
20
30
|
archived: number;
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
|
-
* Archive a single notification on the server side.
|
|
33
|
+
* Archive a single notification on the server side. Verifies ownership before
|
|
34
|
+
* the active → history move and persists the app-supplied `expireAt`.
|
|
24
35
|
*/
|
|
25
36
|
export declare function archiveNotificationHelper(db: ServerFirestore, config: NotificationSystemConfig, input: ArchiveInput): Promise<ArchiveResult>;
|
|
26
37
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archiveNotificationHelper.d.ts","sourceRoot":"","sources":["../../src/server/archiveNotificationHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,wBAAwB,EACxB,YAAY,EACb,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,UAAU,YAAY;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,
|
|
1
|
+
{"version":3,"file":"archiveNotificationHelper.d.ts","sourceRoot":"","sources":["../../src/server/archiveNotificationHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,wBAAwB,EACxB,YAAY,EACb,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,UAAU,YAAY;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,YAAY,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,eAAe;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,aAAa;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AA+BD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,wBAAwB,EAChC,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,aAAa,CAAC,CAsCxB;AAED;;GAEG;AACH,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,wBAAwB,EAChC,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,aAAa,CAAC,CA8DxB"}
|
|
@@ -3,26 +3,48 @@
|
|
|
3
3
|
* Moves active → history with ArchivalInfo, deletes from active.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Assert the caller may archive a notification in this category.
|
|
7
|
+
*
|
|
8
|
+
* - personal: the active doc's `targetUserId` must equal `callerUid`.
|
|
9
|
+
* - shared: the category must be `shared` and the caller must be an admin.
|
|
10
|
+
*
|
|
11
|
+
* Throws on mismatch so the callable surfaces a permission error.
|
|
12
|
+
*/
|
|
13
|
+
function assertCanArchive(audienceType, activeData, callerUid, callerIsAdmin) {
|
|
14
|
+
if (audienceType === 'shared') {
|
|
15
|
+
if (!callerIsAdmin) {
|
|
16
|
+
throw new Error('[notification-core] Permission denied: shared notifications can only be archived by an admin');
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (activeData.targetUserId !== callerUid) {
|
|
21
|
+
throw new Error('[notification-core] Permission denied: caller does not own this notification');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Archive a single notification on the server side. Verifies ownership before
|
|
26
|
+
* the active → history move and persists the app-supplied `expireAt`.
|
|
7
27
|
*/
|
|
8
28
|
export async function archiveNotificationHelper(db, config, input) {
|
|
9
|
-
const { notificationId, category,
|
|
29
|
+
const { notificationId, category, callerUid, callerIsAdmin, archivalInfo, expireAt } = input;
|
|
10
30
|
const categoryConfig = config.categories[category];
|
|
11
31
|
if (!categoryConfig) {
|
|
12
32
|
throw new Error(`[notification-core] Unknown category: ${category}`);
|
|
13
33
|
}
|
|
14
34
|
const activePath = categoryConfig.activePath;
|
|
15
|
-
const historyPath = categoryConfig.historyPath(
|
|
35
|
+
const historyPath = categoryConfig.historyPath(callerUid);
|
|
16
36
|
const activeRef = db.doc(`${activePath}/${notificationId}`);
|
|
17
37
|
const activeSnap = await activeRef.get();
|
|
18
38
|
if (!activeSnap.exists) {
|
|
19
39
|
return { success: true, archived: 0 };
|
|
20
40
|
}
|
|
21
41
|
const activeData = activeSnap.data();
|
|
42
|
+
assertCanArchive(categoryConfig.audienceType, activeData, callerUid, callerIsAdmin);
|
|
22
43
|
const historyDoc = {
|
|
23
44
|
...activeData,
|
|
24
45
|
id: notificationId,
|
|
25
46
|
archival: archivalInfo,
|
|
47
|
+
...(expireAt !== undefined ? { expireAt } : {}),
|
|
26
48
|
...(categoryConfig.audienceType === 'shared'
|
|
27
49
|
? { handledBy: archivalInfo.archivedBy }
|
|
28
50
|
: {}),
|
|
@@ -38,20 +60,25 @@ export async function archiveNotificationHelper(db, config, input) {
|
|
|
38
60
|
* Archive all active notifications for a user/category on the server side.
|
|
39
61
|
*/
|
|
40
62
|
export async function archiveAllNotificationsHelper(db, config, input) {
|
|
41
|
-
const { category,
|
|
63
|
+
const { category, callerUid, callerIsAdmin, archivalInfo, expireAt } = input;
|
|
42
64
|
const categoryConfig = config.categories[category];
|
|
43
65
|
if (!categoryConfig) {
|
|
44
66
|
throw new Error(`[notification-core] Unknown category: ${category}`);
|
|
45
67
|
}
|
|
46
|
-
const activePath = categoryConfig.activePath;
|
|
47
|
-
const historyPath = categoryConfig.historyPath(userId);
|
|
48
68
|
const isPersonal = categoryConfig.audienceType === 'personal';
|
|
69
|
+
// Shared archive-all is an admin-only bulk action; personal stays bounded to
|
|
70
|
+
// the caller by the `targetUserId == callerUid` query filter below.
|
|
71
|
+
if (!isPersonal && !callerIsAdmin) {
|
|
72
|
+
throw new Error('[notification-core] Permission denied: shared notifications can only be archived by an admin');
|
|
73
|
+
}
|
|
74
|
+
const activePath = categoryConfig.activePath;
|
|
75
|
+
const historyPath = categoryConfig.historyPath(callerUid);
|
|
49
76
|
let totalArchived = 0;
|
|
50
77
|
let hasMore = true;
|
|
51
78
|
while (hasMore) {
|
|
52
79
|
const baseQuery = db.collection(activePath);
|
|
53
80
|
const q = isPersonal
|
|
54
|
-
? baseQuery.where('targetUserId', '==',
|
|
81
|
+
? baseQuery.where('targetUserId', '==', callerUid).orderBy('updatedAt', 'desc').limit(50)
|
|
55
82
|
: baseQuery.orderBy('updatedAt', 'desc').limit(50);
|
|
56
83
|
const snapshot = await q.get();
|
|
57
84
|
if (snapshot.empty) {
|
|
@@ -66,6 +93,7 @@ export async function archiveAllNotificationsHelper(db, config, input) {
|
|
|
66
93
|
...data,
|
|
67
94
|
id: docSnap.id,
|
|
68
95
|
archival: archivalInfo,
|
|
96
|
+
...(expireAt !== undefined ? { expireAt } : {}),
|
|
69
97
|
...(categoryConfig.audienceType === 'shared'
|
|
70
98
|
? { handledBy: archivalInfo.archivedBy }
|
|
71
99
|
: {}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archiveNotificationHelper.js","sourceRoot":"","sources":["../../src/server/archiveNotificationHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"archiveNotificationHelper.js","sourceRoot":"","sources":["../../src/server/archiveNotificationHelper.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ import type { ServerFirestore, NotificationHelper } from './types.js';
|
|
|
18
18
|
* const notifier = createNotificationHelper(db as any, TTT_NOTIFICATION_CONFIG);
|
|
19
19
|
*
|
|
20
20
|
* // Auto-selects delivery mode based on type config:
|
|
21
|
-
* await notifier.send({ type: 'content_report', actorId: '...',
|
|
21
|
+
* await notifier.send({ type: 'content_report', actorId: '...', metadata: {...} });
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
export declare function createNotificationHelper(db: ServerFirestore, config: NotificationSystemConfig): NotificationHelper;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createNotificationHelper.d.ts","sourceRoot":"","sources":["../../src/server/createNotificationHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,KAAK,EACV,eAAe,EAEf,kBAAkB,EACnB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"createNotificationHelper.d.ts","sourceRoot":"","sources":["../../src/server/createNotificationHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,KAAK,EACV,eAAe,EAEf,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAOpB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,wBAAwB,GAC/B,kBAAkB,CAwIpB"}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const DEFAULT_COUNT_CAP = 5000;
|
|
6
6
|
const DEFAULT_ACTOR_CAP = 5;
|
|
7
|
+
/** Firestore hard limit on writes per batch. */
|
|
8
|
+
const MAX_BATCH_WRITES = 500;
|
|
7
9
|
/**
|
|
8
10
|
* Create a notification helper bound to a Firestore instance and config.
|
|
9
11
|
*
|
|
@@ -18,7 +20,7 @@ const DEFAULT_ACTOR_CAP = 5;
|
|
|
18
20
|
* const notifier = createNotificationHelper(db as any, TTT_NOTIFICATION_CONFIG);
|
|
19
21
|
*
|
|
20
22
|
* // Auto-selects delivery mode based on type config:
|
|
21
|
-
* await notifier.send({ type: 'content_report', actorId: '...',
|
|
23
|
+
* await notifier.send({ type: 'content_report', actorId: '...', metadata: {...} });
|
|
22
24
|
* ```
|
|
23
25
|
*/
|
|
24
26
|
export function createNotificationHelper(db, config) {
|
|
@@ -38,7 +40,7 @@ export function createNotificationHelper(db, config) {
|
|
|
38
40
|
return typeof defaultPath === 'function' ? defaultPath(metadata) : defaultPath;
|
|
39
41
|
}
|
|
40
42
|
async function sendRealTime(input) {
|
|
41
|
-
const { type, actorId,
|
|
43
|
+
const { type, actorId, targetUserId, metadata } = input;
|
|
42
44
|
const { typeConfig, categoryConfig } = getTypeConfig(type);
|
|
43
45
|
const activePath = categoryConfig.activePath;
|
|
44
46
|
const dedupKey = typeConfig.dedupKeyPattern(metadata);
|
|
@@ -57,16 +59,15 @@ export function createNotificationHelper(db, config) {
|
|
|
57
59
|
const existingData = existingDoc.data();
|
|
58
60
|
const currentCount = existingData.count || 1;
|
|
59
61
|
const currentActorIds = existingData.latestActorIds || [];
|
|
60
|
-
|
|
61
|
-
// Cap actors
|
|
62
|
+
// Cap actors (id-only — newest first, deduped)
|
|
62
63
|
const newActorIds = [actorId, ...currentActorIds.filter((id) => id !== actorId)].slice(0, actorCap);
|
|
63
|
-
const newActorNames = [actorName, ...currentActorNames.filter((_, i) => currentActorIds[i] !== actorId)].slice(0, actorCap);
|
|
64
64
|
const newCount = Math.min(currentCount + 1, countCap);
|
|
65
65
|
await existingDoc.ref.update({
|
|
66
66
|
count: newCount,
|
|
67
67
|
latestActorIds: newActorIds,
|
|
68
|
-
latestActorNames: newActorNames,
|
|
69
68
|
message: typeConfig.messagePattern(metadata, newCount),
|
|
69
|
+
// New activity on an existing notification re-lights the unread badge.
|
|
70
|
+
seenAt: 0,
|
|
70
71
|
updatedAt: Date.now(),
|
|
71
72
|
});
|
|
72
73
|
}
|
|
@@ -83,9 +84,10 @@ export function createNotificationHelper(db, config) {
|
|
|
83
84
|
message: typeConfig.messagePattern(metadata, 1),
|
|
84
85
|
count: 1,
|
|
85
86
|
latestActorIds: [actorId],
|
|
86
|
-
latestActorNames: [actorName],
|
|
87
87
|
targetPath,
|
|
88
88
|
metadata,
|
|
89
|
+
// Active docs are created unseen so the unread count() predicate matches.
|
|
90
|
+
seenAt: 0,
|
|
89
91
|
createdAt: now,
|
|
90
92
|
updatedAt: now,
|
|
91
93
|
};
|
|
@@ -93,18 +95,38 @@ export function createNotificationHelper(db, config) {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
async function queueForBatch(input) {
|
|
96
|
-
const
|
|
98
|
+
const pendingDoc = buildPendingDoc(input, Date.now());
|
|
99
|
+
await db.collection(pendingPath).add(pendingDoc);
|
|
100
|
+
}
|
|
101
|
+
function buildPendingDoc(input, now) {
|
|
102
|
+
const { type, actorId, targetUserId, metadata } = input;
|
|
97
103
|
const { typeConfig } = getTypeConfig(type);
|
|
98
|
-
|
|
104
|
+
// Pending docs carry NO seenAt — seenAt lives only on active docs, set by
|
|
105
|
+
// the materializer (sendRealTime / processBatchHelper).
|
|
106
|
+
return {
|
|
99
107
|
type,
|
|
100
108
|
category: typeConfig.category,
|
|
101
109
|
targetUserId: targetUserId ?? null,
|
|
102
110
|
actorId,
|
|
103
|
-
actorName,
|
|
104
111
|
metadata,
|
|
105
|
-
createdAt:
|
|
112
|
+
createdAt: now,
|
|
106
113
|
};
|
|
107
|
-
|
|
114
|
+
}
|
|
115
|
+
async function queueManyForBatch(inputs) {
|
|
116
|
+
if (inputs.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
const pendingCollection = db.collection(pendingPath);
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
// Write in ≤500-write chunks (the Firestore batch limit).
|
|
121
|
+
for (let i = 0; i < inputs.length; i += MAX_BATCH_WRITES) {
|
|
122
|
+
const chunk = inputs.slice(i, i + MAX_BATCH_WRITES);
|
|
123
|
+
const batch = db.batch();
|
|
124
|
+
for (const input of chunk) {
|
|
125
|
+
const ref = pendingCollection.doc();
|
|
126
|
+
batch.set(ref, buildPendingDoc(input, now));
|
|
127
|
+
}
|
|
128
|
+
await batch.commit();
|
|
129
|
+
}
|
|
108
130
|
}
|
|
109
131
|
async function send(input) {
|
|
110
132
|
const { typeConfig } = getTypeConfig(input.type);
|
|
@@ -113,6 +135,6 @@ export function createNotificationHelper(db, config) {
|
|
|
113
135
|
}
|
|
114
136
|
return queueForBatch(input);
|
|
115
137
|
}
|
|
116
|
-
return { send, sendRealTime, queueForBatch };
|
|
138
|
+
return { send, sendRealTime, queueForBatch, queueManyForBatch };
|
|
117
139
|
}
|
|
118
140
|
//# sourceMappingURL=createNotificationHelper.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createNotificationHelper.js","sourceRoot":"","sources":["../../src/server/createNotificationHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAC/B,MAAM,iBAAiB,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"createNotificationHelper.js","sourceRoot":"","sources":["../../src/server/createNotificationHelper.ts"],"names":[],"mappings":"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"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createNotificationHelper } from './createNotificationHelper.js';
|
|
2
2
|
export { processBatchHelper } from './processBatchHelper.js';
|
|
3
3
|
export { archiveNotificationHelper, archiveAllNotificationsHelper, } from './archiveNotificationHelper.js';
|
|
4
|
+
export { markSeenHelper } from './markSeenHelper.js';
|
|
4
5
|
export type { ServerFirestore, ServerCollectionRef, ServerQuery, ServerQuerySnapshot, ServerDocSnapshot, ServerDocRef, ServerWriteBatch, CreateNotificationInput, NotificationHelper, } from './types.js';
|
|
5
6
|
//# 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;
|
|
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;AAGrD,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
export { createNotificationHelper } from './createNotificationHelper.js';
|
|
3
3
|
export { processBatchHelper } from './processBatchHelper.js';
|
|
4
4
|
export { archiveNotificationHelper, archiveAllNotificationsHelper, } from './archiveNotificationHelper.js';
|
|
5
|
+
export { markSeenHelper } from './markSeenHelper.js';
|
|
5
6
|
//# 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"}
|
|
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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark-seen helper — used by the app's `markNotificationsSeen` callable.
|
|
3
|
+
* Batches `seenAt = now` writes for a set of active notification doc refs.
|
|
4
|
+
*/
|
|
5
|
+
import type { ServerFirestore, ServerDocRef } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Stamp `seenAt` on a set of active notification doc refs, writing in
|
|
8
|
+
* ≤500-write chunks per commit (the Firestore batch limit).
|
|
9
|
+
*
|
|
10
|
+
* Generic — no domain knowledge. The consuming app's callable supplies the
|
|
11
|
+
* ownership-filtered set of refs (personal `targetUserId == uid`); this helper
|
|
12
|
+
* just stamps them. It does not archive and writes no audit event.
|
|
13
|
+
*/
|
|
14
|
+
export declare function markSeenHelper(db: ServerFirestore, docRefs: ServerDocRef[], seenAt: number): Promise<{
|
|
15
|
+
updated: number;
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=markSeenHelper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markSeenHelper.d.ts","sourceRoot":"","sources":["../../src/server/markSeenHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAKhE;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,eAAe,EACnB,OAAO,EAAE,YAAY,EAAE,EACvB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAe9B"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark-seen helper — used by the app's `markNotificationsSeen` callable.
|
|
3
|
+
* Batches `seenAt = now` writes for a set of active notification doc refs.
|
|
4
|
+
*/
|
|
5
|
+
/** Firestore hard limit on writes per batch. */
|
|
6
|
+
const MAX_BATCH_WRITES = 500;
|
|
7
|
+
/**
|
|
8
|
+
* Stamp `seenAt` on a set of active notification doc refs, writing in
|
|
9
|
+
* ≤500-write chunks per commit (the Firestore batch limit).
|
|
10
|
+
*
|
|
11
|
+
* Generic — no domain knowledge. The consuming app's callable supplies the
|
|
12
|
+
* ownership-filtered set of refs (personal `targetUserId == uid`); this helper
|
|
13
|
+
* just stamps them. It does not archive and writes no audit event.
|
|
14
|
+
*/
|
|
15
|
+
export async function markSeenHelper(db, docRefs, seenAt) {
|
|
16
|
+
if (docRefs.length === 0) {
|
|
17
|
+
return { updated: 0 };
|
|
18
|
+
}
|
|
19
|
+
for (let i = 0; i < docRefs.length; i += MAX_BATCH_WRITES) {
|
|
20
|
+
const chunk = docRefs.slice(i, i + MAX_BATCH_WRITES);
|
|
21
|
+
const batch = db.batch();
|
|
22
|
+
for (const ref of chunk) {
|
|
23
|
+
batch.update(ref, { seenAt });
|
|
24
|
+
}
|
|
25
|
+
await batch.commit();
|
|
26
|
+
}
|
|
27
|
+
return { updated: docRefs.length };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=markSeenHelper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markSeenHelper.js","sourceRoot":"","sources":["../../src/server/markSeenHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,gDAAgD;AAChD,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,EAAmB,EACnB,OAAuB,EACvB,MAAc;IAEd,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACxB,CAAC;IAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,gBAAgB,EAAE,CAAC;QAC1D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC;QACrD,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;YACxB,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QAChC,CAAC;QACD,MAAM,KAAK,CAAC,MAAM,EAAE,CAAC;IACvB,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processBatchHelper.d.ts","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAuB,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAOlD,UAAU,kBAAkB;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,
|
|
1
|
+
{"version":3,"file":"processBatchHelper.d.ts","sourceRoot":"","sources":["../../src/server/processBatchHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAuB,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAOlD,UAAU,kBAAkB;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CAgK7B"}
|