@spfn/notification 0.1.0-beta.23 → 0.1.0-beta.24

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.js CHANGED
@@ -210,8 +210,8 @@ import { createHmac, createHash, timingSafeEqual } from "crypto";
210
210
  function hashClickUrl(url) {
211
211
  return createHash("sha256").update(url).digest("hex").slice(0, 16);
212
212
  }
213
- function toBase64Url(buffer) {
214
- return buffer.toString("base64url");
213
+ function toBase64Url(buffer2) {
214
+ return buffer2.toString("base64url");
215
215
  }
216
216
  function fromBase64Url(str) {
217
217
  return Buffer.from(str, "base64url").toString("utf8");
@@ -620,8 +620,8 @@ function registerBuiltinTemplates() {
620
620
  }
621
621
 
622
622
  // src/services/notification.service.ts
623
- import { create, createMany, findOne, findMany, updateOne, count } from "@spfn/core/db";
624
- import { desc, eq, and, gte, lte } from "drizzle-orm";
623
+ import { create, createMany, findOne, findMany, updateOne, count, getDatabase } from "@spfn/core/db";
624
+ import { desc, eq, and, gte, lte, count as drizzleCount } from "drizzle-orm";
625
625
 
626
626
  // src/entities/schema.ts
627
627
  import { createSchema } from "@spfn/core/db";
@@ -863,15 +863,33 @@ async function countNotifications(options = {}) {
863
863
  return await count(notifications);
864
864
  }
865
865
  async function getNotificationStats(options = {}) {
866
- const [total, scheduled, pending, sent, failed, cancelled] = await Promise.all([
867
- countNotifications(options),
868
- countNotifications({ ...options, status: "scheduled" }),
869
- countNotifications({ ...options, status: "pending" }),
870
- countNotifications({ ...options, status: "sent" }),
871
- countNotifications({ ...options, status: "failed" }),
872
- countNotifications({ ...options, status: "cancelled" })
873
- ]);
874
- return { total, scheduled, pending, sent, failed, cancelled };
866
+ const conditions = [];
867
+ if (options.channel) {
868
+ conditions.push(eq(notifications.channel, options.channel));
869
+ }
870
+ if (options.from) {
871
+ conditions.push(gte(notifications.createdAt, options.from));
872
+ }
873
+ if (options.to) {
874
+ conditions.push(lte(notifications.createdAt, options.to));
875
+ }
876
+ const rows = await getDatabase("read").select({ status: notifications.status, count: drizzleCount() }).from(notifications).where(conditions.length > 0 ? and(...conditions) : void 0).groupBy(notifications.status);
877
+ const stats = {
878
+ total: 0,
879
+ scheduled: 0,
880
+ pending: 0,
881
+ sent: 0,
882
+ failed: 0,
883
+ cancelled: 0
884
+ };
885
+ for (const row of rows) {
886
+ const n = Number(row.count);
887
+ stats.total += n;
888
+ if (row.status && row.status in stats) {
889
+ stats[row.status] = n;
890
+ }
891
+ }
892
+ return stats;
875
893
  }
876
894
  async function findScheduledNotifications(options = {}) {
877
895
  const conditions = [eq(notifications.status, "scheduled")];
@@ -1809,11 +1827,11 @@ function IsTemplateLiteralFinite(schema) {
1809
1827
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/template-literal/generate.mjs
1810
1828
  var TemplateLiteralGenerateError = class extends TypeBoxError {
1811
1829
  };
1812
- function* GenerateReduce(buffer) {
1813
- if (buffer.length === 1)
1814
- return yield* buffer[0];
1815
- for (const left of buffer[0]) {
1816
- for (const right of GenerateReduce(buffer.slice(1))) {
1830
+ function* GenerateReduce(buffer2) {
1831
+ if (buffer2.length === 1)
1832
+ return yield* buffer2[0];
1833
+ for (const left of buffer2[0]) {
1834
+ for (const right of GenerateReduce(buffer2.slice(1))) {
1817
1835
  yield `${left}${right}`;
1818
1836
  }
1819
1837
  }
@@ -3646,15 +3664,8 @@ async function sendEmail(params) {
3646
3664
  log2.error("Email send failed", { to: recipients, subject, error: result.error });
3647
3665
  }
3648
3666
  if (historyId && isHistoryEnabled()) {
3649
- try {
3650
- if (result.success) {
3651
- await markNotificationSent(historyId, result.messageId);
3652
- } else {
3653
- await markNotificationFailed(historyId, result.error || "Unknown error");
3654
- }
3655
- } catch (error) {
3656
- log2.warn("Failed to update notification history record", error);
3657
- }
3667
+ const update = result.success ? markNotificationSent(historyId, result.messageId) : markNotificationFailed(historyId, result.error || "Unknown error");
3668
+ update.catch((error) => log2.warn("Failed to update notification history record", error));
3658
3669
  }
3659
3670
  return result;
3660
3671
  }
@@ -3951,8 +3962,7 @@ async function sendSMS(params) {
3951
3962
  };
3952
3963
  }
3953
3964
  const provider = getProvider2();
3954
- const results = [];
3955
- for (const recipient of recipients) {
3965
+ const sendOne = async (recipient) => {
3956
3966
  const normalizedPhone = normalizePhoneNumber(recipient);
3957
3967
  const internalParams = {
3958
3968
  to: normalizedPhone,
@@ -3981,18 +3991,12 @@ async function sendSMS(params) {
3981
3991
  log4.error("SMS send failed", { to: normalizedPhone, error: result.error });
3982
3992
  }
3983
3993
  if (historyId && isHistoryEnabled()) {
3984
- try {
3985
- if (result.success) {
3986
- await markNotificationSent(historyId, result.messageId);
3987
- } else {
3988
- await markNotificationFailed(historyId, result.error || "Unknown error");
3989
- }
3990
- } catch (error) {
3991
- log4.warn("Failed to update notification history record", error);
3992
- }
3994
+ const update = result.success ? markNotificationSent(historyId, result.messageId) : markNotificationFailed(historyId, result.error || "Unknown error");
3995
+ update.catch((error) => log4.warn("Failed to update notification history record", error));
3993
3996
  }
3994
- results.push(result);
3995
- }
3997
+ return result;
3998
+ };
3999
+ const results = await runWithConcurrency(recipients, sendOne);
3996
4000
  const allSuccess = results.every((r) => r.success);
3997
4001
  const messageIds = results.filter((r) => r.messageId).map((r) => r.messageId).join(",");
3998
4002
  const errors = results.filter((r) => r.error).map((r) => r.error).join("; ");
@@ -4271,15 +4275,8 @@ async function sendSlack(params) {
4271
4275
  log6.error("Slack send failed", { error: result.error });
4272
4276
  }
4273
4277
  if (historyId && isHistoryEnabled()) {
4274
- try {
4275
- if (result.success) {
4276
- await markNotificationSent(historyId, result.messageId);
4277
- } else {
4278
- await markNotificationFailed(historyId, result.error || "Unknown error");
4279
- }
4280
- } catch (error) {
4281
- log6.warn("Failed to update notification history record", error);
4282
- }
4278
+ const update = result.success ? markNotificationSent(historyId, result.messageId) : markNotificationFailed(historyId, result.error || "Unknown error");
4279
+ update.catch((error) => log6.warn("Failed to update notification history record", error));
4283
4280
  }
4284
4281
  return result;
4285
4282
  }
@@ -4791,37 +4788,66 @@ import { route } from "@spfn/core/route";
4791
4788
  import { defineRouter } from "@spfn/core/route";
4792
4789
 
4793
4790
  // src/tracking/tracking.service.ts
4794
- import { create as create2, getDatabase } from "@spfn/core/db";
4795
- import { eq as eq2, and as and2, gte as gte2, lte as lte2, count as drizzleCount, countDistinct } from "drizzle-orm";
4791
+ import { getDatabase as getDatabase2 } from "@spfn/core/db";
4792
+ import { eq as eq2, and as and2, gte as gte2, lte as lte2, count as drizzleCount2, countDistinct } from "drizzle-orm";
4796
4793
  import { logger as logger7 } from "@spfn/core/logger";
4797
4794
  var log7 = logger7.child("@spfn/notification:tracking");
4795
+ var FLUSH_SIZE = 200;
4796
+ var FLUSH_INTERVAL_MS = 2e3;
4797
+ var MAX_BUFFER = 1e4;
4798
+ var buffer = [];
4799
+ var flushTimer = null;
4800
+ async function flushTrackingEvents() {
4801
+ if (flushTimer) {
4802
+ clearTimeout(flushTimer);
4803
+ flushTimer = null;
4804
+ }
4805
+ if (buffer.length === 0) {
4806
+ return;
4807
+ }
4808
+ const batch = buffer.splice(0, buffer.length);
4809
+ try {
4810
+ await getDatabase2("write").insert(trackingEvents).values(batch);
4811
+ } catch (error) {
4812
+ log7.warn(`Failed to flush ${batch.length} tracking events`, error);
4813
+ }
4814
+ }
4815
+ function enqueueTrackingEvent(event) {
4816
+ if (buffer.length >= MAX_BUFFER) {
4817
+ log7.warn("Tracking buffer full \u2014 dropping event");
4818
+ return;
4819
+ }
4820
+ buffer.push(event);
4821
+ if (buffer.length >= FLUSH_SIZE) {
4822
+ void flushTrackingEvents();
4823
+ } else if (!flushTimer) {
4824
+ flushTimer = setTimeout(() => void flushTrackingEvents(), FLUSH_INTERVAL_MS);
4825
+ flushTimer.unref?.();
4826
+ }
4827
+ }
4798
4828
  function recordOpenEvent(notificationId, meta) {
4799
- create2(trackingEvents, {
4829
+ enqueueTrackingEvent({
4800
4830
  notificationId,
4801
4831
  type: "open",
4802
4832
  ipAddress: meta?.ipAddress,
4803
4833
  userAgent: meta?.userAgent
4804
- }).catch((error) => {
4805
- log7.warn("Failed to record open event", error);
4806
4834
  });
4807
4835
  }
4808
4836
  function recordClickEvent(notificationId, linkIndex, linkUrl, meta) {
4809
- create2(trackingEvents, {
4837
+ enqueueTrackingEvent({
4810
4838
  notificationId,
4811
4839
  type: "click",
4812
4840
  linkUrl,
4813
4841
  linkIndex,
4814
4842
  ipAddress: meta?.ipAddress,
4815
4843
  userAgent: meta?.userAgent
4816
- }).catch((error) => {
4817
- log7.warn("Failed to record click event", error);
4818
4844
  });
4819
4845
  }
4820
4846
  async function getTrackingStats(notificationId) {
4821
- const db = getDatabase("read");
4847
+ const db = getDatabase2("read");
4822
4848
  const rows = await db.select({
4823
4849
  type: trackingEvents.type,
4824
- total: drizzleCount(),
4850
+ total: drizzleCount2(),
4825
4851
  unique: countDistinct(trackingEvents.ipAddress)
4826
4852
  }).from(trackingEvents).where(eq2(trackingEvents.notificationId, notificationId)).groupBy(trackingEvents.type);
4827
4853
  const openRow = rows.find((r) => r.type === "open");
@@ -4834,7 +4860,7 @@ async function getTrackingStats(notificationId) {
4834
4860
  };
4835
4861
  }
4836
4862
  async function getEngagementStats(options = {}) {
4837
- const db = getDatabase("read");
4863
+ const db = getDatabase2("read");
4838
4864
  const sentConditions = [eq2(notifications.status, "sent")];
4839
4865
  if (options.channel) {
4840
4866
  sentConditions.push(eq2(notifications.channel, options.channel));
@@ -4845,7 +4871,7 @@ async function getEngagementStats(options = {}) {
4845
4871
  if (options.to) {
4846
4872
  sentConditions.push(lte2(notifications.createdAt, options.to));
4847
4873
  }
4848
- const [sentResult] = await db.select({ count: drizzleCount() }).from(notifications).where(and2(...sentConditions));
4874
+ const [sentResult] = await db.select({ count: drizzleCount2() }).from(notifications).where(and2(...sentConditions));
4849
4875
  const sent = Number(sentResult?.count ?? 0);
4850
4876
  if (sent === 0) {
4851
4877
  return { sent: 0, opened: 0, clicked: 0, openRate: 0, clickRate: 0 };
@@ -4880,11 +4906,11 @@ async function getEngagementStats(options = {}) {
4880
4906
  };
4881
4907
  }
4882
4908
  async function getClickDetails(notificationId) {
4883
- const db = getDatabase("read");
4909
+ const db = getDatabase2("read");
4884
4910
  const rows = await db.select({
4885
4911
  linkUrl: trackingEvents.linkUrl,
4886
4912
  linkIndex: trackingEvents.linkIndex,
4887
- totalClicks: drizzleCount(),
4913
+ totalClicks: drizzleCount2(),
4888
4914
  uniqueClicks: countDistinct(trackingEvents.ipAddress)
4889
4915
  }).from(trackingEvents).where(
4890
4916
  and2(