@mohasinac/appkit 3.2.9 → 3.2.11

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.
@@ -10,6 +10,7 @@
10
10
  * <AsyncFacetSection loadOptions={load} ... />
11
11
  */
12
12
  import { apiClient } from "../../../../http/ApiClient";
13
+ import { normalizeError } from "../../../../errors/normalize";
13
14
  const PAGE_SIZE = 25;
14
15
  function buildUrl(base, q, page, extra) {
15
16
  const params = new URLSearchParams({
@@ -29,7 +30,7 @@ async function fetchPage(url, mapItem) {
29
30
  };
30
31
  }
31
32
  catch (_err) {
32
- // PaginatedSelect degrades to an empty page on load error — caller can retry by re-typing
33
+ void normalizeError(_err); // PaginatedSelect degrades to empty page; caller can retry by re-typing
33
34
  return { items: [], hasMore: false };
34
35
  }
35
36
  }
@@ -1,3 +1,4 @@
1
+ import type { JsonObject } from "../../../../schemas/types";
1
2
  type IdentityOperation = "signInWithPassword" | "signInWithCustomToken";
2
3
  /**
3
4
  * Calls the Firebase Identity Toolkit REST API with one retry for transient
@@ -8,5 +9,5 @@ type IdentityOperation = "signInWithPassword" | "signInWithCustomToken";
8
9
  * into a session cookie. Centralised here so the URL, Content-Type, and retry
9
10
  * config are in one place.
10
11
  */
11
- export declare function callFirebaseIdentityToolkit<T extends Record<string, unknown>>(operation: IdentityOperation, body: Record<string, unknown>, apiKey: string): Promise<T>;
12
+ export declare function callFirebaseIdentityToolkit<T extends JsonObject>(operation: IdentityOperation, body: JsonObject, apiKey: string): Promise<T>;
12
13
  export {};
@@ -17,7 +17,10 @@ export async function runCleanupRtdbEvents(ctx) {
17
17
  if (staleAuthIds.length > 0) {
18
18
  await Promise.all(staleAuthIds.flatMap((id) => [
19
19
  rtdb.ref(`auth_events/${id}`).remove(),
20
- auth.deleteUser(`auth_event_${id}`).catch(console.error),
20
+ auth.deleteUser(`auth_event_${id}`).catch((err) => {
21
+ void normalizeError(err);
22
+ ctx.logger.warn("cleanupRtdbEvents: deleteUser skipped — may already be absent", { userId: `auth_event_${id}`, error: err instanceof Error ? err.message : String(err) });
23
+ }),
21
24
  ]));
22
25
  ctx.logger.info("Auth events removed", { count: staleAuthIds.length });
23
26
  }
@@ -1,3 +1,4 @@
1
+ import { normalizeError } from "../../../errors/normalize";
1
2
  function defaultSink(event) {
2
3
  if (typeof window === "undefined") {
3
4
  // SSR / Node — best-effort console log; production servers can swap
@@ -32,7 +33,7 @@ export const actionTracker = {
32
33
  const result = currentSink(event);
33
34
  // Swallow promise rejections so the caller path is never affected.
34
35
  if (result && typeof result.catch === "function") {
35
- result.catch(console.error);
36
+ result.catch((err) => { void normalizeError(err); });
36
37
  }
37
38
  }
38
39
  catch {
@@ -11,6 +11,7 @@
11
11
  import { NextResponse } from "next/server.js";
12
12
  import { blogRepository } from "../../repository/blog.repository";
13
13
  import { normalizeError } from "../../../../errors/normalize";
14
+ import { safeFireAndForget } from "../../../../utils/safe-fire-forget";
14
15
  function toBlogPost(doc) {
15
16
  return {
16
17
  ...doc,
@@ -28,7 +29,7 @@ export async function GET(_request, context) {
28
29
  return NextResponse.json({ success: false, error: "Blog post not found" }, { status: 404 });
29
30
  }
30
31
  // Increment view count fire-and-forget — must not block response
31
- blogRepository.incrementViews(post.id).catch(console.error);
32
+ safeFireAndForget(blogRepository.incrementViews(post.id), "blog: incrementViews");
32
33
  // Related posts: same category, latest 3, excluding current
33
34
  const related = (await blogRepository.findRelated(post.category, post.id, 3)).map(toBlogPost);
34
35
  const body = { post: toBlogPost(post), related };
@@ -8,6 +8,8 @@
8
8
  import { ALLOWED_LIVE_COLLECTIONS } from "../schemas/firestore";
9
9
  import { productRepository, storeRepository, userRepository, reviewRepository, orderRepository, } from "../../../repositories";
10
10
  import { getAdminDb, getFirestoreCount } from "../../../providers/db-firebase";
11
+ import { normalizeError } from "../../../errors/normalize";
12
+ import { serverLogger } from "../../../monitoring/server-logger";
11
13
  /**
12
14
  * Fetch live stats for all requested metrics.
13
15
  * Preset metrics are deduped so each Firestore query runs at most once.
@@ -27,22 +29,22 @@ export async function fetchLiveStats(requests) {
27
29
  if (neededPresets.has("total_listings")) {
28
30
  tasks.push(productRepository.count()
29
31
  .then((n) => { resolvedPresets.total_listings = String(n); })
30
- .catch(console.error));
32
+ .catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: total_listings query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
31
33
  }
32
34
  if (neededPresets.has("verified_sellers")) {
33
35
  tasks.push(storeRepository.count()
34
36
  .then((n) => { resolvedPresets.verified_sellers = String(n); })
35
- .catch(console.error));
37
+ .catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: verified_sellers query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
36
38
  }
37
39
  if (neededPresets.has("total_buyers")) {
38
40
  tasks.push(userRepository.countByRole("user")
39
41
  .then((n) => { resolvedPresets.total_buyers = String(n); })
40
- .catch(console.error));
42
+ .catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: total_buyers query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
41
43
  }
42
44
  if (neededPresets.has("total_orders")) {
43
45
  tasks.push(orderRepository.count()
44
46
  .then((n) => { resolvedPresets.total_orders = String(n); })
45
- .catch(console.error));
47
+ .catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: total_orders query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
46
48
  }
47
49
  if (neededPresets.has("total_reviews") || neededPresets.has("platform_rating")) {
48
50
  tasks.push(reviewRepository.findAll()
@@ -57,7 +59,7 @@ export async function fetchLiveStats(requests) {
57
59
  resolvedPresets.platform_rating = avg.toFixed(1) + "★";
58
60
  }
59
61
  })
60
- .catch(console.error));
62
+ .catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: reviews/rating query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
61
63
  }
62
64
  // --- Collection-query metrics ------------------------------------------------
63
65
  const queryRequests = requests.filter((r) => r.source === "live-collection" && r.collectionQuery !== undefined);
@@ -1,6 +1,7 @@
1
1
  "use server";
2
2
  import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
3
3
  import { scammerRepository } from "../repository/scammer.repository";
4
+ import { safeFireAndForget } from "../../../utils/safe-fire-forget";
4
5
  /**
5
6
  * List verified scammer profiles for the public registry page.
6
7
  * Accepts sieve-style searchParams from Next.js page props.
@@ -43,7 +44,7 @@ export async function getPublicScammerById(id) {
43
44
  if (!doc || doc.status !== "verified")
44
45
  return null;
45
46
  // Increment views — non-blocking.
46
- scammerRepository.incrementViews(doc.id).catch(console.error);
47
+ safeFireAndForget(scammerRepository.incrementViews(doc.id), "scam: incrementViews");
47
48
  return doc;
48
49
  }
49
50
  /**
@@ -20,6 +20,9 @@
20
20
  * ```
21
21
  */
22
22
  import { getAdminStorageLite } from "../db-firebase/admin-storage-lite";
23
+ import { withRetry } from "../../http/retry";
24
+ import { normalizeError } from "../../errors/normalize";
25
+ import { serverLogger } from "../../monitoring/server-logger";
23
26
  function getBucket() {
24
27
  const bucketName = process.env.FIREBASE_ADMIN_STORAGE_BUCKET?.trim() ??
25
28
  process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET?.trim();
@@ -29,75 +32,126 @@ function getBucket() {
29
32
  }
30
33
  export const firebaseStorageProvider = {
31
34
  async upload(file, storagePath, options) {
32
- const bucket = getBucket();
33
- const fileRef = bucket.file(storagePath);
34
- await fileRef.save(file, {
35
- metadata: {
36
- contentType: options?.contentType ?? "application/octet-stream",
37
- cacheControl: options?.cacheControl ?? "public, max-age=31536000",
38
- metadata: options?.metadata ?? {},
39
- },
40
- public: options?.isPublic !== false,
41
- });
42
- const [metadata] = await fileRef.getMetadata();
43
- const url = options?.isPublic !== false
44
- ? `https://storage.googleapis.com/${bucket.name}/${storagePath}`
45
- : await fileRef
46
- .getSignedUrl({
47
- action: "read",
48
- expires: Date.now() + 60 * 60 * 1000, // 1 hour
49
- })
50
- .then(([u]) => u);
51
- return {
52
- path: storagePath,
53
- url,
54
- contentType: metadata.contentType,
55
- size: Number(metadata.size),
56
- updatedAt: metadata.updated,
57
- };
35
+ try {
36
+ const bucket = getBucket();
37
+ const fileRef = bucket.file(storagePath);
38
+ await withRetry(() => fileRef.save(file, {
39
+ metadata: {
40
+ contentType: options?.contentType ?? "application/octet-stream",
41
+ cacheControl: options?.cacheControl ?? "public, max-age=31536000",
42
+ metadata: options?.metadata ?? {},
43
+ },
44
+ public: options?.isPublic !== false,
45
+ }), 2, 300);
46
+ const [metadata] = await withRetry(() => fileRef.getMetadata(), 2, 300);
47
+ const url = options?.isPublic !== false
48
+ ? `https://storage.googleapis.com/${bucket.name}/${storagePath}`
49
+ : await withRetry(() => fileRef
50
+ .getSignedUrl({
51
+ action: "read",
52
+ expires: Date.now() + 60 * 60 * 1000,
53
+ })
54
+ .then(([u]) => u), 2, 300);
55
+ return {
56
+ path: storagePath,
57
+ url,
58
+ contentType: metadata.contentType,
59
+ size: Number(metadata.size),
60
+ updatedAt: metadata.updated,
61
+ };
62
+ }
63
+ catch (err) {
64
+ void normalizeError(err);
65
+ serverLogger.error("storage-provider: upload failed", {
66
+ storagePath,
67
+ error: err instanceof Error ? err.message : String(err),
68
+ });
69
+ throw err;
70
+ }
58
71
  },
59
72
  async delete(storagePath) {
60
- await getBucket().file(storagePath).delete({ ignoreNotFound: true });
73
+ try {
74
+ await withRetry(() => getBucket().file(storagePath).delete({ ignoreNotFound: true }), 2, 300);
75
+ }
76
+ catch (err) {
77
+ void normalizeError(err);
78
+ serverLogger.error("storage-provider: delete failed", {
79
+ storagePath,
80
+ error: err instanceof Error ? err.message : String(err),
81
+ });
82
+ throw err;
83
+ }
61
84
  },
62
85
  getPublicUrl(storagePath) {
63
86
  const bucket = getBucket();
64
87
  return `https://storage.googleapis.com/${bucket.name}/${storagePath}`;
65
88
  },
66
89
  async getSignedUrl(storagePath, expiresInSeconds = 3600) {
67
- const [url] = await getBucket()
68
- .file(storagePath)
69
- .getSignedUrl({
70
- action: "read",
71
- expires: Date.now() + expiresInSeconds * 1000,
72
- });
73
- return url;
90
+ try {
91
+ const [url] = await withRetry(() => getBucket()
92
+ .file(storagePath)
93
+ .getSignedUrl({
94
+ action: "read",
95
+ expires: Date.now() + expiresInSeconds * 1000,
96
+ }), 2, 300);
97
+ return url;
98
+ }
99
+ catch (err) {
100
+ void normalizeError(err);
101
+ serverLogger.error("storage-provider: getSignedUrl failed", {
102
+ storagePath,
103
+ error: err instanceof Error ? err.message : String(err),
104
+ });
105
+ throw err;
106
+ }
74
107
  },
75
108
  async copy(from, to) {
76
- const bucket = getBucket();
77
- const src = bucket.file(from);
78
- const dest = bucket.file(to);
79
- await src.copy(dest);
80
- const [metadata] = await dest.getMetadata();
81
- return {
82
- path: to,
83
- url: `https://storage.googleapis.com/${bucket.name}/${to}`,
84
- contentType: metadata.contentType,
85
- size: Number(metadata.size),
86
- updatedAt: metadata.updated,
87
- };
88
- },
89
- async list(prefix) {
90
- const bucket = getBucket();
91
- const [files] = await bucket.getFiles({ prefix });
92
- return Promise.all(files.map(async (f) => {
93
- const [metadata] = await f.getMetadata();
109
+ try {
110
+ const bucket = getBucket();
111
+ const src = bucket.file(from);
112
+ const dest = bucket.file(to);
113
+ await withRetry(() => src.copy(dest), 2, 300);
114
+ const [metadata] = await withRetry(() => dest.getMetadata(), 2, 300);
94
115
  return {
95
- path: f.name,
96
- url: `https://storage.googleapis.com/${bucket.name}/${f.name}`,
116
+ path: to,
117
+ url: `https://storage.googleapis.com/${bucket.name}/${to}`,
97
118
  contentType: metadata.contentType,
98
119
  size: Number(metadata.size),
99
120
  updatedAt: metadata.updated,
100
121
  };
101
- }));
122
+ }
123
+ catch (err) {
124
+ void normalizeError(err);
125
+ serverLogger.error("storage-provider: copy failed", {
126
+ from,
127
+ to,
128
+ error: err instanceof Error ? err.message : String(err),
129
+ });
130
+ throw err;
131
+ }
132
+ },
133
+ async list(prefix) {
134
+ try {
135
+ const bucket = getBucket();
136
+ const [files] = await withRetry(() => bucket.getFiles({ prefix }), 2, 300);
137
+ return Promise.all(files.map(async (f) => {
138
+ const [metadata] = await withRetry(() => f.getMetadata(), 2, 300);
139
+ return {
140
+ path: f.name,
141
+ url: `https://storage.googleapis.com/${bucket.name}/${f.name}`,
142
+ contentType: metadata.contentType,
143
+ size: Number(metadata.size),
144
+ updatedAt: metadata.updated,
145
+ };
146
+ }));
147
+ }
148
+ catch (err) {
149
+ void normalizeError(err);
150
+ serverLogger.error("storage-provider: list failed", {
151
+ prefix,
152
+ error: err instanceof Error ? err.message : String(err),
153
+ });
154
+ throw err;
155
+ }
102
156
  },
103
157
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "3.2.9",
3
+ "version": "3.2.11",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"