@lunora/notify 1.0.0-alpha.76 → 1.0.0-alpha.77
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 +11 -1
- package/dist/index.d.mts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/buildEngine-x7r_sr3M.mjs +1 -0
- package/dist/packem_shared/createNotify-DnO2QQ8p.mjs +1 -0
- package/dist/web.d.mts +19 -3
- package/dist/web.d.ts +19 -3
- package/dist/web.mjs +1 -1
- package/package.json +3 -3
- package/dist/packem_shared/buildEngine-BMMRV5cw.mjs +0 -1
- package/dist/packem_shared/createNotify-BB43NqyO.mjs +0 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ export default defineNotify({
|
|
|
22
22
|
});
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`.dev.vars` (scaffolded by `lunora dev` from `@lunora/config`'s package-secrets registry):
|
|
25
|
+
`.dev.vars` (scaffolded by `lunora dev` from `@lunora/config`'s package-secrets registry — only for the channels `defineNotify` configures):
|
|
26
26
|
|
|
27
27
|
```
|
|
28
28
|
VAPID_PUBLIC_KEY=<your-vapid-public-key>
|
|
@@ -43,6 +43,10 @@ const { replacedEndpoint, subscription } = await subscribeToPush({ serviceWorker
|
|
|
43
43
|
await client.mutation("registerDevice", { replacedEndpoint, subscription });
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
Without `serviceWorkerUrl`, `subscribeToPush` uses the page's existing
|
|
47
|
+
registration: await your own `navigator.serviceWorker.register()` first, or the
|
|
48
|
+
call rejects with "no service worker is registered" instead of waiting for it.
|
|
49
|
+
|
|
46
50
|
`replacedEndpoint` is set only after a **VAPID key rotation**: the stale browser
|
|
47
51
|
subscription is dropped and a new one minted, and the new one has a new endpoint
|
|
48
52
|
— hence a new store id — so it never upserts over the old row. Every send to that
|
|
@@ -128,6 +132,12 @@ export const announce = action.input({ title: v.string(), body: v.string() }).ac
|
|
|
128
132
|
await ctx.push.send(subscriptionId, { title: "Hi", body: "…" });
|
|
129
133
|
```
|
|
130
134
|
|
|
135
|
+
`send` and `broadcast` take per-message Web Push delivery hints, overriding the channel's `urgency` / `ttl` (FCM targets ignore both):
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
await ctx.push.broadcast({ title: "Approval needed", body: "…", urgency: "high", ttl: 3600 }, { userId });
|
|
139
|
+
```
|
|
140
|
+
|
|
131
141
|
Multi-channel through `ctx.notify.send`:
|
|
132
142
|
|
|
133
143
|
```ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FcmConfig } from '@visulima/notification/providers/fcm';
|
|
2
2
|
export type { FcmConfig } from '@visulima/notification/providers/fcm';
|
|
3
|
-
import {
|
|
3
|
+
import { WebPushConfig, PushSubscriptionLike } from '@visulima/notification/providers/web-push';
|
|
4
4
|
export type { PushSubscriptionLike, WebPushConfig } from '@visulima/notification/providers/web-push';
|
|
5
5
|
import { ChatPayload, Receipt, InAppPayload, PushPayload, NotificationMessage, WebhookPayload, Notification, Provider } from '@visulima/notification';
|
|
6
6
|
export type { ChatPayload, InAppPayload, NotificationMessage, PushPayload, Receipt, WebhookPayload } from '@visulima/notification';
|
|
@@ -374,8 +374,24 @@ interface PushOwner {
|
|
|
374
374
|
*/
|
|
375
375
|
userId: string | null | undefined;
|
|
376
376
|
}
|
|
377
|
-
/**
|
|
378
|
-
|
|
377
|
+
/**
|
|
378
|
+
* A push payload without its `to` target — the facade derives `to` from the
|
|
379
|
+
* stored subscription — plus per-message Web Push delivery hints.
|
|
380
|
+
*/
|
|
381
|
+
interface PushContent extends Omit<PushPayload, "to"> {
|
|
382
|
+
/**
|
|
383
|
+
* Seconds the push service keeps this message for an offline device (the
|
|
384
|
+
* Web Push `TTL` header), overriding the channel's `ttl`. Web Push only: FCM
|
|
385
|
+
* targets ignore it.
|
|
386
|
+
*/
|
|
387
|
+
ttl?: number;
|
|
388
|
+
/**
|
|
389
|
+
* Delivery urgency of this message (the Web Push `Urgency` header),
|
|
390
|
+
* overriding the channel's `urgency` — e.g. `"high"` for an approval
|
|
391
|
+
* request, the default for the rest. Web Push only: FCM targets ignore it.
|
|
392
|
+
*/
|
|
393
|
+
urgency?: WebPushConfig["urgency"];
|
|
394
|
+
}
|
|
379
395
|
/**
|
|
380
396
|
* The multi-channel notification facade — spliced onto ctx as `ctx.notify`.
|
|
381
397
|
* `send` delivers a fully-specified multi-channel message through the engine;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FcmConfig } from '@visulima/notification/providers/fcm';
|
|
2
2
|
export type { FcmConfig } from '@visulima/notification/providers/fcm';
|
|
3
|
-
import {
|
|
3
|
+
import { WebPushConfig, PushSubscriptionLike } from '@visulima/notification/providers/web-push';
|
|
4
4
|
export type { PushSubscriptionLike, WebPushConfig } from '@visulima/notification/providers/web-push';
|
|
5
5
|
import { ChatPayload, Receipt, InAppPayload, PushPayload, NotificationMessage, WebhookPayload, Notification, Provider } from '@visulima/notification';
|
|
6
6
|
export type { ChatPayload, InAppPayload, NotificationMessage, PushPayload, Receipt, WebhookPayload } from '@visulima/notification';
|
|
@@ -374,8 +374,24 @@ interface PushOwner {
|
|
|
374
374
|
*/
|
|
375
375
|
userId: string | null | undefined;
|
|
376
376
|
}
|
|
377
|
-
/**
|
|
378
|
-
|
|
377
|
+
/**
|
|
378
|
+
* A push payload without its `to` target — the facade derives `to` from the
|
|
379
|
+
* stored subscription — plus per-message Web Push delivery hints.
|
|
380
|
+
*/
|
|
381
|
+
interface PushContent extends Omit<PushPayload, "to"> {
|
|
382
|
+
/**
|
|
383
|
+
* Seconds the push service keeps this message for an offline device (the
|
|
384
|
+
* Web Push `TTL` header), overriding the channel's `ttl`. Web Push only: FCM
|
|
385
|
+
* targets ignore it.
|
|
386
|
+
*/
|
|
387
|
+
ttl?: number;
|
|
388
|
+
/**
|
|
389
|
+
* Delivery urgency of this message (the Web Push `Urgency` header),
|
|
390
|
+
* overriding the channel's `urgency` — e.g. `"high"` for an approval
|
|
391
|
+
* request, the default for the rest. Web Push only: FCM targets ignore it.
|
|
392
|
+
*/
|
|
393
|
+
urgency?: WebPushConfig["urgency"];
|
|
394
|
+
}
|
|
379
395
|
/**
|
|
380
396
|
* The multi-channel notification facade — spliced onto ctx as `ctx.notify`.
|
|
381
397
|
* `send` delivers a fully-specified multi-channel message through the engine;
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{FCM_ENV_KEYS as e,WEB_PUSH_ENV_KEYS as t,fcmFromEnv as i,webPushFromEnv as f}from"./packem_shared/FCM_ENV_KEYS-BbhPScGH.mjs";import{defineNotify as n,isNotifyDefinition as m}from"./packem_shared/defineNotify-DVPpVkU0.mjs";import{createNotify as u}from"./packem_shared/createNotify-
|
|
1
|
+
import{FCM_ENV_KEYS as e,WEB_PUSH_ENV_KEYS as t,fcmFromEnv as i,webPushFromEnv as f}from"./packem_shared/FCM_ENV_KEYS-BbhPScGH.mjs";import{defineNotify as n,isNotifyDefinition as m}from"./packem_shared/defineNotify-DVPpVkU0.mjs";import{createNotify as u}from"./packem_shared/createNotify-DnO2QQ8p.mjs";import{buildEngine as E,routingPushProvider as d}from"./packem_shared/buildEngine-x7r_sr3M.mjs";import{enqueuePushBroadcast as P,runPushBroadcastPage as c}from"./packem_shared/enqueuePushBroadcast-C9tO0BZj.mjs";import{d1SubscriptionStore as b}from"./packem_shared/d1SubscriptionStore-h6gSRs-m.mjs";import{memorySubscriptionStore as h}from"./packem_shared/memorySubscriptionStore-q8KD05cV.mjs";import{f as _,i as y,n as v,t as w,w as B}from"./packem_shared/normalize-B5B9QUol.mjs";export{e as FCM_ENV_KEYS,t as WEB_PUSH_ENV_KEYS,E as buildEngine,u as createNotify,b as d1SubscriptionStore,n as defineNotify,P as enqueuePushBroadcast,i as fcmFromEnv,_ as fcmId,y as isGoneError,m as isNotifyDefinition,h as memorySubscriptionStore,v as normalizeRegisterInput,d as routingPushProvider,c as runPushBroadcastPage,w as targetOf,f as webPushFromEnv,B as webPushId};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as d,isLunoraError as U}from"@lunora/errors";import{createNotification as L}from"@visulima/notification";import{retryMiddleware as _}from"@visulima/notification/middleware";import{createFcmProvider as B}from"@visulima/notification/providers/fcm";import{createWebPushProvider as H}from"@visulima/notification/providers/web-push";import{a as F,p as k,b as G,d as M,i as z}from"./normalize-B5B9QUol.mjs";const V=(e,t)=>{if(e.size<t)return;const r=e.keys().next().value;r!==void 0&&e.delete(r)},j="https://cloudflare-dns.com/dns-query",E=1,A=28,Y=2e3,Q=(e,t)=>{if(t===E){const r=k(e);return r===void 0||G(r)}return M(e.toLowerCase())},I=async(e,t,r)=>{try{const s=await fetch(`${j}?name=${encodeURIComponent(e)}&type=${String(t)}`,{headers:{accept:"application/dns-json"},signal:AbortSignal.timeout(r)});if(!s.ok)return;const n=await s.json();return n.Status===void 0||n.Status===0?n.Answer??[]:[]}catch{return}},J=async(e,t=Y)=>{const r=F(e);if(r.includes(":")||k(r)!==void 0)return{kind:"skipped"};const[s,n]=await Promise.all([I(r,E,t),I(r,A,t)]);if(s===void 0||n===void 0)return{kind:"failed"};const o=[...s,...n].filter(i=>i.type===E||i.type===A);for(const i of o)if(Q(i.data,i.type))return{address:i.data,kind:"private"};return o.length===0?{kind:"unresolved"}:{kind:"public"}},$=e=>{let t=e;if(typeof e=="string"){if(!e.startsWith("{"))return;try{t=JSON.parse(e)}catch{return}}const r=t?.endpoint;return typeof r=="string"?r:void 0},K=e=>$(e)===void 0?"fcm":"web-push",h="lunora-push-router",y=new Map,W=256,q=async(e,t)=>{if(t!==void 0&&t.length>0)return;let r;try{({hostname:r}=new URL(e))}catch{return}const s=y.get(r),n=s??J(r),o=await n;if(s===void 0&&(o.kind==="private"||o.kind==="public")&&(V(y,W),y.set(r,n)),o.kind==="private")throw new d("FORBIDDEN",`@lunora/notify: web-push endpoint host "${r}" resolves to a private/internal address (${o.address}); refusing to send (DNS-rebinding guard)`);if(o.kind==="unresolved")throw new d("FORBIDDEN",`@lunora/notify: web-push endpoint host "${r}" did not resolve to any address; refusing to send (DNS-rebinding guard)`);if(o.kind==="failed")throw new d("FORBIDDEN",`@lunora/notify: could not verify where web-push endpoint host "${r}" resolves (the DNS-over-HTTPS lookup failed); refusing to send (DNS-rebinding guard)`)},x=(e,t)=>{const r=[e.provider,t.provider].filter(n=>n!==void 0),s=[...e.recipients??[],...t.recipients??[]];return{...e,messageId:[e.messageId,t.messageId].join(","),response:[e.response,t.response],sent:e.sent&&t.sent,timestamp:new Date(Math.max(e.timestamp.getTime(),t.timestamp.getTime())),...r.length>0?{provider:r.join(",")}:{},...s.length>0?{recipients:s}:{}}},O=e=>e instanceof Error?e.message:typeof e=="string"?e:void 0,N="PUSH_PARTIALLY_DELIVERED",C=e=>U(e)&&e.code===N,X=e=>{if(!C(e))return;const{data:t}=e;return t==="fcm"||t==="web-push"?t:void 0},m=(e,t)=>{if(!e.success||!t.success){if(e.success||t.success){const[s,n]=e.success?["web-push","fcm"]:["fcm","web-push"],o=e.success?t.error:e.error;return{error:new d(N,`@lunora/notify: push partially delivered — the ${s} group was accepted, the ${n} group failed and re-attempting it did not recover it (${O(o)??"unknown error"}). The whole send is not retried: that would re-send to the ${s} targets that already received it. Re-send to the ${n} targets only.`,{cause:o,data:n}),success:!1}}return{error:new AggregateError([e.error,t.error],"@lunora/notify: both push target groups failed"),success:!1}}const r=e.data===void 0||t.data===void 0?e.data??t.data:x(e.data,t.data);return r===void 0?{success:!0}:{data:r,success:!0}},v=e=>z(O(e)),p=5,Z=3e4,ee=e=>{if(e.provider!==h)return[e.provider];const{to:t}=e.payload,r=Array.isArray(t)?t:[t],s=new Set(r.map(n=>K(n)));return s.size===0?[e.provider]:[...s].map(n=>`${e.provider}:${n}`)},te=(e,t)=>e.failures>=p&&t-e.openedAt<Z,re=(e,t,r)=>{const s=X(e),n=s===void 0?void 0:`${h}:${s}`,o=n!==void 0&&t.includes(n);for(const[i,a]of r.entries())o&&t[i]!==n?a.failures=0:(a.failures+=1,a.failures>=p&&(a.openedAt=Date.now()))},ne=()=>{const e=new Map,t=r=>{const s=e.get(r);if(s!==void 0)return s;const n={failures:0,openedAt:0};return e.set(r,n),n};return async(r,s)=>{const n=ee(r),o=n.map(u=>t(u)),i=Date.now();if(o.every(u=>te(u,i)))return{error:new d("SERVICE_UNAVAILABLE",`@lunora/notify: circuit open for "${n.join('", "')}" after ${p.toString()} consecutive failures`),success:!1};for(const u of o)u.failures>=p&&(u.failures=p-1);const a=await s(r);if(a.success)for(const u of o)u.failures=0;else v(a.error)||re(a.error,n,o);return a}},se=2,b=async(e,t)=>{try{return await e.send(t)}catch(r){return{error:r,success:!1}}},oe=new Set(["high","low","normal","very-low"]),ie=e=>oe.has(e),ve=e=>{if(e!==void 0&&(typeof e!="number"||!Number.isInteger(e)||e<0))throw new d("BAD_REQUEST",`@lunora/notify: push ttl must be a non-negative integer number of seconds, got ${typeof e=="number"?String(e):typeof e}`);return e},we=e=>{if(e!==void 0&&!ie(e))throw new d("BAD_REQUEST",`@lunora/notify: push urgency must be "very-low", "low", "normal" or "high", got ${JSON.stringify(e)}`)},ae=e=>{const t=_({baseDelay:e.retryBaseDelay,retries:se,shouldRetry:n=>!v(n)}),r=async(n,o,i)=>v(i.error)?i:t({channel:"push",payload:o,provider:h},async a=>b(n,a.payload)),s=n=>{const o=n===void 0?e.fcm:e.webPush;if(o===void 0)throw new Error(n===void 0?"@lunora/notify: received an FCM token target but no `fcm` channel is configured":"@lunora/notify: received a web-push target but no `webPush` channel is configured");return o};return{channel:"push",id:h,initialize:async()=>{await e.webPush?.initialize(),await e.fcm?.initialize()},isAvailable:()=>(e.webPush??e.fcm)!==void 0,send:async n=>{const o=Array.isArray(n.to)?n.to:[n.to];if(o.length===0)throw new d("BAD_REQUEST","@lunora/notify: push send has no recipients — `to` is an empty array");const i=o.map(c=>$(c));for(const c of i)c!==void 0&&await q(c,e.allowedPushOrigins);const a=o.filter((c,g)=>i[g]!==void 0),u=o.filter((c,g)=>i[g]===void 0),w=i.find(c=>c!==void 0);if(u.length===0&&w!==void 0)return s(w).send(n);if(a.length===0)return s(void 0).send(n);const R=c=>({...n,to:c.length===1&&c[0]!==void 0?c[0]:c}),D=s(w),P=s(void 0),S=R(a),T=R(u),[f,l]=await Promise.all([b(D,S),b(P,T)]);return f.success&&!l.success?m(f,await r(P,T,l)):l.success&&!f.success?m(await r(D,S,f),l):m(f,l)}}},ce=(e,t={})=>e.use(_({baseDelay:t.retryBaseDelay,shouldRetry:r=>!v(r)&&!C(r)})).use(ne()),ge=e=>{const t=e.webPush===void 0?void 0:H(e.webPush),r=e.fcm===void 0?void 0:B(e.fcm),s={};return(t!==void 0||r!==void 0)&&(s.push=ae({allowedPushOrigins:e.allowedPushOrigins,fcm:r,webPush:t})),e.chat!==void 0&&(s.chat=e.chat),e.inApp!==void 0&&(s.inapp=e.inApp),e.webhook!==void 0&&(s.webhook=e.webhook),ce(L(s))};export{ce as attachResilience,ge as buildEngine,ve as checkPushTtl,we as checkPushUrgency,ae as routingPushProvider};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as v}from"@lunora/errors";import{buildEngine as R,checkPushTtl as x,checkPushUrgency as C}from"./buildEngine-x7r_sr3M.mjs";import{memorySubscriptionStore as B}from"./memorySubscriptionStore-q8KD05cV.mjs";import{n as L,t as U,i as W}from"./normalize-B5B9QUol.mjs";const j=250,w=(o,u)=>typeof o=="function"?o(u):o,S=o=>o.successful?void 0:o.errorMessages.join("; "),G=(o,u,d)=>o.successful?"accepted":W(u,d)?"gone":"failed",Q=async(o,u,d)=>{const l=Array.from({length:o.length});let h=0;const g=async()=>{for(;h<o.length;){const f=h;h+=1,l[f]=await d(o[f])}};return await Promise.all(Array.from({length:Math.min(u,o.length)},()=>g())),l},Z=(o,u)=>({allowedPushOrigins:o.allowedPushOrigins,chat:w(o.chat,u),fcm:w(o.fcm,u),inApp:w(o.inApp,u),webhook:w(o.webhook,u),webPush:w(o.webPush,u)}),M=new WeakMap,q=(o,u)=>{let d=M.get(o);d===void 0&&(d=new WeakMap,M.set(o,d));let l=d.get(u);return l===void 0&&(l={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},d.set(u,l)),l},te=(o,u,d={})=>{const l=q(o,u);let h;d.engine===void 0?(l.engine??=R(Z(o,u)),h=l.engine):h=d.engine,l.store??=o.store?.(u);let{store:g}=l;g===void 0&&(l.fallbackStore??=B(),!d.silent&&!l.warnedNoStore&&(l.warnedNoStore=!0,console.warn("@lunora/notify: no `store` configured — using a non-durable in-memory subscription store. Configure `store: (env) => d1SubscriptionStore(env.DB)` for production.")),g=l.fallbackStore);const f=g,I=Math.max(1,d.concurrency??o.concurrency??10),P=Math.max(1,d.broadcastPageSize??o.broadcastPageSize??j),{log:T,metrics:b}=d,p=(e,t,r,n=1)=>{b?.count("notify.send",n,{channel:e,provider:t??e,status:r})},y=(e,t,r)=>{T?.warn(`notify ${e} delivery failed`,{channel:e,provider:t??e,status:"failed",...r})},A=(e,t)=>{b?.count("notify.skipped",1,{channel:e,reason:t})},_=()=>{const e=o.allowedPushOrigins!==void 0&&o.allowedPushOrigins.length>0;d.silent||e||l.warnedNoPushOriginAllowlist||(l.warnedNoPushOriginAllowlist=!0,console.warn("@lunora/notify: Web Push registered without `allowedPushOrigins` — endpoints are guarded by a string classifier at register time and a best-effort DNS re-check at send time, both of which are defeatable. Set `allowedPushOrigins` to the exact push-service origins for a hard guarantee."))},D=async e=>(await f.list(e)).map(({keys:r,token:n,...a})=>a),$=async e=>{if(typeof e!="string")return e;const t=await f.get(e);if(t===void 0)throw new v("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${e}"`);return t},O=async(e,t,r)=>{let n,a,s;try{n=await h.sendToChannel("push",{...t,to:U(e)}),a=S(n),s=G(n,a,e.kind)}catch(c){s="failed",a=c instanceof Error?c.message:String(c)}try{s==="accepted"?await f.markStatus(e.id,"ok"):s==="gone"?await f.delete(e.id):await f.markStatus(e.id,"failed",a)}catch{}return s==="failed"&&y("push",e.kind,{error:a,subscriptionId:e.id,userId:e.userId??null}),r&&p("push",e.kind,s),{error:a,receipt:n,status:s}},z=async(e,t)=>{const r=await Q(t,I,async s=>{const{error:c,status:i}=await O(s,e,!1);return{error:c,kind:s.kind,status:i,subscription:s}}),n=new Map;for(const{kind:s,status:c}of r){const i=`${s} ${c}`,m=n.get(i);m===void 0?n.set(i,{count:1,kind:s,status:c}):m.count+=1}for(const{count:s,kind:c,status:i}of n.values())p("push",c,i,s);const a=r.map(({error:s,status:c,subscription:i})=>c==="accepted"?{id:i.id,status:"ok"}:c==="gone"?{error:s,id:i.id,status:"expired"}:{error:s,id:i.id,status:"failed"});return{failed:a.filter(s=>s.status==="failed").length,outcomes:a,pruned:a.filter(s=>s.status==="expired").length,sent:a.filter(s=>s.status==="ok").length,total:a.length}},E=async(e,t)=>{if(x(e.ttl),C(e.urgency),t?.limit!==void 0&&t.limit<=0)return{nextCursor:void 0,result:{failed:0,outcomes:[],pruned:0,sent:0,total:0}};const r=t?.limit!==void 0&&t.limit>0?Math.trunc(t.limit):void 0,n=r===void 0?P:Math.min(r,P),a=await f.list({after:t?.after,kind:t?.kind,limit:n+1,userId:t?.userId}),s=t?.after===void 0?a:a.filter(F=>F.id>t.after),c=s.length>n,i=c?s.slice(0,n):s;i.length===0&&t?.after===void 0&&A("push","no-subscriptions-matched");const m=await z(e,i);return{nextCursor:c?i[i.length-1]?.id:void 0,result:m}},N={broadcast:async(e,t)=>{const r={failed:0,outcomes:[],pruned:0,sent:0,total:0};let n=t?.after;const a=t?.limit;if(a!==void 0&&a<=0)return r;for(;;){const s=a===void 0?{...t,after:n}:{...t,after:n,limit:a-r.total},{nextCursor:c,result:i}=await E(e,s);if(r.failed+=i.failed,r.pruned+=i.pruned,r.sent+=i.sent,r.total+=i.total,r.outcomes.push(...i.outcomes),a!==void 0&&r.total>=a||c===void 0||c===n)break;n=c}return r},broadcastPage:E,list:e=>D(e),register:e=>("token"in e||_(),f.put(L(e,void 0,{allowedPushOrigins:o.allowedPushOrigins}))),send:async(e,t)=>{x(t.ttl),C(t.urgency);const{error:r,receipt:n}=await O(await $(e),t,!0);if(n===void 0)throw new v("INTERNAL",`@lunora/notify: push send failed: ${r??"unknown error"}`);return n},unregister:async(e,t)=>{await f.deleteOwned(e,t.userId??null)}},k=async(e,t)=>{if(h.getProvider(e)===void 0)throw A(e,"channel-not-configured"),new v("BAD_REQUEST",`@lunora/notify: the "${e}" channel is not configured in defineNotify(...)`);const r=await h.sendToChannel(e,t),n=r.successful?"accepted":"failed";return p(e,r.provider,n),n==="failed"&&y(e,r.provider,{error:S(r)}),r};return{notify:{chat:e=>k("chat",e),inApp:e=>k("inapp",e),push:N,send:async e=>{const t=await h.send(e);for(const r of t){const n=r.channel??"unknown",a=r.successful?"accepted":"failed";p(n,r.provider,a),a==="failed"&&y(n,r.provider,{error:S(r)})}return t},webhook:e=>k("webhook",e)},push:N}};export{te as createNotify};
|
package/dist/web.d.mts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long {@link existingRegistration} waits for a REGISTERED service worker to
|
|
3
|
+
* become active before failing (ms). Boundless, `navigator.serviceWorker.ready`
|
|
4
|
+
* never rejects — a worker stuck in `installing` forever (failed install,
|
|
5
|
+
* hung update script) leaves callers hanging. After this bound, the caller
|
|
6
|
+
* fails instead. Generous: activation normally completes in well under a second
|
|
7
|
+
* after the script is fetched; the bound only trades a permanent hang for a
|
|
8
|
+
* bounded failure.
|
|
9
|
+
*/
|
|
10
|
+
declare const READY_WAIT_TIMEOUT_MS = 3e4;
|
|
1
11
|
interface SerializedPushSubscription {
|
|
2
12
|
endpoint: string;
|
|
3
13
|
expirationTime: number | null;
|
|
@@ -38,7 +48,8 @@ interface SubscribeToPushOptions {
|
|
|
38
48
|
scope?: string;
|
|
39
49
|
/**
|
|
40
50
|
* URL of the service worker script to register. Omit to reuse the page's
|
|
41
|
-
*
|
|
51
|
+
* existing registration (`navigator.serviceWorker.ready`); the call rejects
|
|
52
|
+
* when the page has none.
|
|
42
53
|
*/
|
|
43
54
|
serviceWorkerUrl?: string;
|
|
44
55
|
/**
|
|
@@ -58,9 +69,14 @@ declare const isPushSupported: () => boolean;
|
|
|
58
69
|
/**
|
|
59
70
|
* Register (or reuse) a service worker and subscribe the browser to Web Push,
|
|
60
71
|
* returning the subscription in serialisable form. Reuses an existing subscription
|
|
61
|
-
* when present. Throws if push is unsupported
|
|
72
|
+
* when present. Throws if push is unsupported, no service worker is registered
|
|
73
|
+
* (without `serviceWorkerUrl`), or the user denies permission.
|
|
74
|
+
*
|
|
75
|
+
* Without `serviceWorkerUrl`, await your own `navigator.serviceWorker.register()`
|
|
76
|
+
* before calling this: a registration still in flight is not found yet, and the
|
|
77
|
+
* call rejects rather than waiting for it.
|
|
62
78
|
*/
|
|
63
79
|
declare const subscribeToPush: (options: SubscribeToPushOptions) => Promise<SubscribeToPushResult>;
|
|
64
80
|
/** Unsubscribe the browser's current Web Push subscription. Returns whether one was removed. */
|
|
65
81
|
declare const unsubscribeFromPush: () => Promise<boolean>;
|
|
66
|
-
export { type SerializedPushSubscription, type SubscribeToPushOptions, type SubscribeToPushResult, isPushSupported, subscribeToPush, unsubscribeFromPush };
|
|
82
|
+
export { READY_WAIT_TIMEOUT_MS, type SerializedPushSubscription, type SubscribeToPushOptions, type SubscribeToPushResult, isPushSupported, subscribeToPush, unsubscribeFromPush };
|
package/dist/web.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long {@link existingRegistration} waits for a REGISTERED service worker to
|
|
3
|
+
* become active before failing (ms). Boundless, `navigator.serviceWorker.ready`
|
|
4
|
+
* never rejects — a worker stuck in `installing` forever (failed install,
|
|
5
|
+
* hung update script) leaves callers hanging. After this bound, the caller
|
|
6
|
+
* fails instead. Generous: activation normally completes in well under a second
|
|
7
|
+
* after the script is fetched; the bound only trades a permanent hang for a
|
|
8
|
+
* bounded failure.
|
|
9
|
+
*/
|
|
10
|
+
declare const READY_WAIT_TIMEOUT_MS = 3e4;
|
|
1
11
|
interface SerializedPushSubscription {
|
|
2
12
|
endpoint: string;
|
|
3
13
|
expirationTime: number | null;
|
|
@@ -38,7 +48,8 @@ interface SubscribeToPushOptions {
|
|
|
38
48
|
scope?: string;
|
|
39
49
|
/**
|
|
40
50
|
* URL of the service worker script to register. Omit to reuse the page's
|
|
41
|
-
*
|
|
51
|
+
* existing registration (`navigator.serviceWorker.ready`); the call rejects
|
|
52
|
+
* when the page has none.
|
|
42
53
|
*/
|
|
43
54
|
serviceWorkerUrl?: string;
|
|
44
55
|
/**
|
|
@@ -58,9 +69,14 @@ declare const isPushSupported: () => boolean;
|
|
|
58
69
|
/**
|
|
59
70
|
* Register (or reuse) a service worker and subscribe the browser to Web Push,
|
|
60
71
|
* returning the subscription in serialisable form. Reuses an existing subscription
|
|
61
|
-
* when present. Throws if push is unsupported
|
|
72
|
+
* when present. Throws if push is unsupported, no service worker is registered
|
|
73
|
+
* (without `serviceWorkerUrl`), or the user denies permission.
|
|
74
|
+
*
|
|
75
|
+
* Without `serviceWorkerUrl`, await your own `navigator.serviceWorker.register()`
|
|
76
|
+
* before calling this: a registration still in flight is not found yet, and the
|
|
77
|
+
* call rejects rather than waiting for it.
|
|
62
78
|
*/
|
|
63
79
|
declare const subscribeToPush: (options: SubscribeToPushOptions) => Promise<SubscribeToPushResult>;
|
|
64
80
|
/** Unsubscribe the browser's current Web Push subscription. Returns whether one was removed. */
|
|
65
81
|
declare const unsubscribeFromPush: () => Promise<boolean>;
|
|
66
|
-
export { type SerializedPushSubscription, type SubscribeToPushOptions, type SubscribeToPushResult, isPushSupported, subscribeToPush, unsubscribeFromPush };
|
|
82
|
+
export { READY_WAIT_TIMEOUT_MS, type SerializedPushSubscription, type SubscribeToPushOptions, type SubscribeToPushResult, isPushSupported, subscribeToPush, unsubscribeFromPush };
|
package/dist/web.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const p=
|
|
1
|
+
const p=e=>{const r=atob(e),t=new Uint8Array(r.length);for(let i=0;i<r.length;i+=1)t[i]=r.codePointAt(i)??0;return t},v=e=>{const r=e.replaceAll("-","+").replaceAll("_","/"),t=r+"=".repeat((4-r.length%4)%4);return p(t)},w=3e4,c="@lunora/notify: the registered service worker never became active",l=e=>v(e),d=(e,r)=>e.length===r.length&&e.every((t,i)=>t===r[i]),b=(e,r)=>{const t=e.options.applicationServerKey;return t===null?!1:d(new Uint8Array(t),l(r))},o=globalThis,u=()=>o.navigator?.serviceWorker!==void 0&&o.PushManager!==void 0&&o.Notification!==void 0,g=async()=>{const e=await navigator.serviceWorker.getRegistration();return e?.active!==null?e:await new Promise((r,t)=>{const i=setTimeout(()=>{t(new Error(c))},3e4);navigator.serviceWorker.ready.then(n=>(clearTimeout(i),r(n),n)).catch(n=>{clearTimeout(i),t(n instanceof Error?n:new Error(String(n)))})})},h=async e=>{if(!u())throw new Error("@lunora/notify: Web Push is not supported in this browser (needs service workers + PushManager + Notification)");let r;if(e.serviceWorkerUrl===void 0){const s=await g();if(s===void 0)throw new Error("@lunora/notify: no service worker is registered for this page — register one first, or pass `serviceWorkerUrl`");r=s}else{const s=e.scope===void 0?void 0:{scope:e.scope};r=await navigator.serviceWorker.register(e.serviceWorkerUrl,s)}const t=await Notification.requestPermission();if(t!=="granted")throw new Error(`@lunora/notify: notification permission was not granted (got "${t}")`);const i=await r.pushManager.getSubscription();let n=null,a;i!==null&&(b(i,e.vapidPublicKey)?n=i:(a=i.endpoint,await i.unsubscribe()));const f=n??await r.pushManager.subscribe({applicationServerKey:l(e.vapidPublicKey),userVisibleOnly:!0});return{replacedEndpoint:a,subscription:f.toJSON()}},y=async()=>{if(!u())return!1;let e;try{e=await g()}catch(t){if(t instanceof Error&&t.message===c)return!1;throw t}if(e===void 0)return!1;const r=await e.pushManager.getSubscription();return r===null?!1:r.unsubscribe()};export{w as READY_WAIT_TIMEOUT_MS,u as isPushSupported,h as subscribeToPush,y as unsubscribeFromPush};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/notify",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.77",
|
|
4
4
|
"description": "Multi-channel notifications for Lunora — ctx.notify / ctx.push over @visulima/notification: edge-safe Web Push + FCM, plus chat, in-app inbox and webhook channels, with subscription storage and queue-backed fan-out",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@lunora/errors": "1.0.0-alpha.51",
|
|
53
|
-
"@visulima/notification": "1.
|
|
52
|
+
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
|
|
53
|
+
"@visulima/notification": "1.2.0"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": "^22.15.0 || >=24.11.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as u,isLunoraError as L}from"@lunora/errors";import{createNotification as U}from"@visulima/notification";import{retryMiddleware as _}from"@visulima/notification/middleware";import{createFcmProvider as B}from"@visulima/notification/providers/fcm";import{createWebPushProvider as H}from"@visulima/notification/providers/web-push";import{a as F,p as k,b as G,d as M,i as z}from"./normalize-B5B9QUol.mjs";const V=(e,t)=>{if(e.size<t)return;const r=e.keys().next().value;r!==void 0&&e.delete(r)},j="https://cloudflare-dns.com/dns-query",E=1,T=28,Y=2e3,K=(e,t)=>{if(t===E){const r=k(e);return r===void 0||G(r)}return M(e.toLowerCase())},S=async(e,t,r)=>{try{const n=await fetch(`${j}?name=${encodeURIComponent(e)}&type=${String(t)}`,{headers:{accept:"application/dns-json"},signal:AbortSignal.timeout(r)});if(!n.ok)return;const s=await n.json();return s.Status===void 0||s.Status===0?s.Answer??[]:[]}catch{return}},W=async(e,t=Y)=>{const r=F(e);if(r.includes(":")||k(r)!==void 0)return{kind:"skipped"};const[n,s]=await Promise.all([S(r,E,t),S(r,T,t)]);if(n===void 0||s===void 0)return{kind:"failed"};const o=[...n,...s].filter(i=>i.type===E||i.type===T);for(const i of o)if(K(i.data,i.type))return{address:i.data,kind:"private"};return o.length===0?{kind:"unresolved"}:{kind:"public"}},O=e=>{let t=e;if(typeof e=="string"){if(!e.startsWith("{"))return;try{t=JSON.parse(e)}catch{return}}const r=t?.endpoint;return typeof r=="string"?r:void 0},q=e=>O(e)===void 0?"fcm":"web-push",h="lunora-push-router",g=new Map,x=256,J=async(e,t)=>{if(t!==void 0&&t.length>0)return;let r;try{({hostname:r}=new URL(e))}catch{return}const n=g.get(r),s=n??W(r),o=await s;if(n===void 0&&(o.kind==="private"||o.kind==="public")&&(V(g,x),g.set(r,s)),o.kind==="private")throw new u("FORBIDDEN",`@lunora/notify: web-push endpoint host "${r}" resolves to a private/internal address (${o.address}); refusing to send (DNS-rebinding guard)`);if(o.kind==="unresolved")throw new u("FORBIDDEN",`@lunora/notify: web-push endpoint host "${r}" did not resolve to any address; refusing to send (DNS-rebinding guard)`);if(o.kind==="failed")throw new u("FORBIDDEN",`@lunora/notify: could not verify where web-push endpoint host "${r}" resolves (the DNS-over-HTTPS lookup failed); refusing to send (DNS-rebinding guard)`)},Q=(e,t)=>{const r=[e.provider,t.provider].filter(s=>s!==void 0),n=[...e.recipients??[],...t.recipients??[]];return{...e,messageId:[e.messageId,t.messageId].join(","),response:[e.response,t.response],sent:e.sent&&t.sent,timestamp:new Date(Math.max(e.timestamp.getTime(),t.timestamp.getTime())),...r.length>0?{provider:r.join(",")}:{},...n.length>0?{recipients:n}:{}}},$=e=>e instanceof Error?e.message:typeof e=="string"?e:void 0,C="PUSH_PARTIALLY_DELIVERED",N=e=>L(e)&&e.code===C,X=e=>{if(!N(e))return;const{data:t}=e;return t==="fcm"||t==="web-push"?t:void 0},m=(e,t)=>{if(!e.success||!t.success){if(e.success||t.success){const[n,s]=e.success?["web-push","fcm"]:["fcm","web-push"],o=e.success?t.error:e.error;return{error:new u(C,`@lunora/notify: push partially delivered — the ${n} group was accepted, the ${s} group failed and re-attempting it did not recover it (${$(o)??"unknown error"}). The whole send is not retried: that would re-send to the ${n} targets that already received it. Re-send to the ${s} targets only.`,{cause:o,data:s}),success:!1}}return{error:new AggregateError([e.error,t.error],"@lunora/notify: both push target groups failed"),success:!1}}const r=e.data===void 0||t.data===void 0?e.data??t.data:Q(e.data,t.data);return r===void 0?{success:!0}:{data:r,success:!0}},v=e=>z($(e)),p=5,Z=3e4,ee=e=>{if(e.provider!==h)return[e.provider];const{to:t}=e.payload,r=Array.isArray(t)?t:[t],n=new Set(r.map(s=>q(s)));return n.size===0?[e.provider]:[...n].map(s=>`${e.provider}:${s}`)},te=(e,t)=>e.failures>=p&&t-e.openedAt<Z,re=(e,t,r)=>{const n=X(e),s=n===void 0?void 0:`${h}:${n}`,o=s!==void 0&&t.includes(s);for(const[i,a]of r.entries())o&&t[i]!==s?a.failures=0:(a.failures+=1,a.failures>=p&&(a.openedAt=Date.now()))},se=()=>{const e=new Map,t=r=>{const n=e.get(r);if(n!==void 0)return n;const s={failures:0,openedAt:0};return e.set(r,s),s};return async(r,n)=>{const s=ee(r),o=s.map(d=>t(d)),i=Date.now();if(o.every(d=>te(d,i)))return{error:new u("SERVICE_UNAVAILABLE",`@lunora/notify: circuit open for "${s.join('", "')}" after ${p.toString()} consecutive failures`),success:!1};for(const d of o)d.failures>=p&&(d.failures=p-1);const a=await n(r);if(a.success)for(const d of o)d.failures=0;else v(a.error)||re(a.error,s,o);return a}},ne=2,b=async(e,t)=>{try{return await e.send(t)}catch(r){return{error:r,success:!1}}},oe=e=>{const t=_({baseDelay:e.retryBaseDelay,retries:ne,shouldRetry:s=>!v(s)}),r=async(s,o,i)=>v(i.error)?i:t({channel:"push",payload:o,provider:h},async a=>b(s,a.payload)),n=s=>{const o=s===void 0?e.fcm:e.webPush;if(o===void 0)throw new Error(s===void 0?"@lunora/notify: received an FCM token target but no `fcm` channel is configured":"@lunora/notify: received a web-push target but no `webPush` channel is configured");return o};return{channel:"push",id:h,initialize:async()=>{await e.webPush?.initialize(),await e.fcm?.initialize()},isAvailable:()=>(e.webPush??e.fcm)!==void 0,send:async s=>{const o=Array.isArray(s.to)?s.to:[s.to];if(o.length===0)throw new u("BAD_REQUEST","@lunora/notify: push send has no recipients — `to` is an empty array");const i=o.map(c=>O(c));for(const c of i)c!==void 0&&await J(c,e.allowedPushOrigins);const a=o.filter((c,y)=>i[y]!==void 0),d=o.filter((c,y)=>i[y]===void 0),w=i.find(c=>c!==void 0);if(d.length===0&&w!==void 0)return n(w).send(s);if(a.length===0)return n(void 0).send(s);const R=c=>({...s,to:c.length===1&&c[0]!==void 0?c[0]:c}),D=n(w),P=n(void 0),A=R(a),I=R(d),[f,l]=await Promise.all([b(D,A),b(P,I)]);return f.success&&!l.success?m(f,await r(P,I,l)):l.success&&!f.success?m(await r(D,A,f),l):m(f,l)}}},ie=(e,t={})=>e.use(_({baseDelay:t.retryBaseDelay,shouldRetry:r=>!v(r)&&!N(r)})).use(se()),pe=e=>{const t=e.webPush===void 0?void 0:H(e.webPush),r=e.fcm===void 0?void 0:B(e.fcm),n={};return(t!==void 0||r!==void 0)&&(n.push=oe({allowedPushOrigins:e.allowedPushOrigins,fcm:r,webPush:t})),e.chat!==void 0&&(n.chat=e.chat),e.inApp!==void 0&&(n.inapp=e.inApp),e.webhook!==void 0&&(n.webhook=e.webhook),ie(U(n))};export{ie as attachResilience,pe as buildEngine,oe as routingPushProvider};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as v}from"@lunora/errors";import{buildEngine as z}from"./buildEngine-BMMRV5cw.mjs";import{memorySubscriptionStore as F}from"./memorySubscriptionStore-q8KD05cV.mjs";import{n as R,t as B,i as L}from"./normalize-B5B9QUol.mjs";const W=250,w=(o,l)=>typeof o=="function"?o(l):o,S=o=>o.successful?void 0:o.errorMessages.join("; "),U=(o,l,d)=>o.successful?"accepted":L(l,d)?"gone":"failed",j=async(o,l,d)=>{const u=Array.from({length:o.length});let h=0;const g=async()=>{for(;h<o.length;){const f=h;h+=1,u[f]=await d(o[f])}};return await Promise.all(Array.from({length:Math.min(l,o.length)},()=>g())),u},G=(o,l)=>({allowedPushOrigins:o.allowedPushOrigins,chat:w(o.chat,l),fcm:w(o.fcm,l),inApp:w(o.inApp,l),webhook:w(o.webhook,l),webPush:w(o.webPush,l)}),x=new WeakMap,Q=(o,l)=>{let d=x.get(o);d===void 0&&(d=new WeakMap,x.set(o,d));let u=d.get(l);return u===void 0&&(u={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},d.set(l,u)),u},Y=(o,l,d={})=>{const u=Q(o,l);let h;d.engine===void 0?(u.engine??=z(G(o,l)),h=u.engine):h=d.engine,u.store??=o.store?.(l);let{store:g}=u;g===void 0&&(u.fallbackStore??=F(),!d.silent&&!u.warnedNoStore&&(u.warnedNoStore=!0,console.warn("@lunora/notify: no `store` configured — using a non-durable in-memory subscription store. Configure `store: (env) => d1SubscriptionStore(env.DB)` for production.")),g=u.fallbackStore);const f=g,C=Math.max(1,d.concurrency??o.concurrency??10),b=Math.max(1,d.broadcastPageSize??o.broadcastPageSize??W),{log:M,metrics:P}=d,p=(e,t,r,n=1)=>{P?.count("notify.send",n,{channel:e,provider:t??e,status:r})},y=(e,t,r)=>{M?.warn(`notify ${e} delivery failed`,{channel:e,provider:t??e,status:"failed",...r})},A=(e,t)=>{P?.count("notify.skipped",1,{channel:e,reason:t})},I=()=>{const e=o.allowedPushOrigins!==void 0&&o.allowedPushOrigins.length>0;d.silent||e||u.warnedNoPushOriginAllowlist||(u.warnedNoPushOriginAllowlist=!0,console.warn("@lunora/notify: Web Push registered without `allowedPushOrigins` — endpoints are guarded by a string classifier at register time and a best-effort DNS re-check at send time, both of which are defeatable. Set `allowedPushOrigins` to the exact push-service origins for a hard guarantee."))},T=async e=>(await f.list(e)).map(({keys:r,token:n,...a})=>a),_=async e=>{if(typeof e!="string")return e;const t=await f.get(e);if(t===void 0)throw new v("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${e}"`);return t},O=async(e,t,r)=>{let n,a,s;try{n=await h.sendToChannel("push",{...t,to:B(e)}),a=S(n),s=U(n,a,e.kind)}catch(c){s="failed",a=c instanceof Error?c.message:String(c)}try{s==="accepted"?await f.markStatus(e.id,"ok"):s==="gone"?await f.delete(e.id):await f.markStatus(e.id,"failed",a)}catch{}return s==="failed"&&y("push",e.kind,{error:a,subscriptionId:e.id,userId:e.userId??null}),r&&p("push",e.kind,s),{error:a,receipt:n,status:s}},D=async(e,t)=>{const r=await j(t,C,async s=>{const{error:c,status:i}=await O(s,e,!1);return{error:c,kind:s.kind,status:i,subscription:s}}),n=new Map;for(const{kind:s,status:c}of r){const i=`${s} ${c}`,m=n.get(i);m===void 0?n.set(i,{count:1,kind:s,status:c}):m.count+=1}for(const{count:s,kind:c,status:i}of n.values())p("push",c,i,s);const a=r.map(({error:s,status:c,subscription:i})=>c==="accepted"?{id:i.id,status:"ok"}:c==="gone"?{error:s,id:i.id,status:"expired"}:{error:s,id:i.id,status:"failed"});return{failed:a.filter(s=>s.status==="failed").length,outcomes:a,pruned:a.filter(s=>s.status==="expired").length,sent:a.filter(s=>s.status==="ok").length,total:a.length}},E=async(e,t)=>{if(t?.limit!==void 0&&t.limit<=0)return{nextCursor:void 0,result:{failed:0,outcomes:[],pruned:0,sent:0,total:0}};const r=t?.limit!==void 0&&t.limit>0?Math.trunc(t.limit):void 0,n=r===void 0?b:Math.min(r,b),a=await f.list({after:t?.after,kind:t?.kind,limit:n+1,userId:t?.userId}),s=t?.after===void 0?a:a.filter($=>$.id>t.after),c=s.length>n,i=c?s.slice(0,n):s;i.length===0&&t?.after===void 0&&A("push","no-subscriptions-matched");const m=await D(e,i);return{nextCursor:c?i[i.length-1]?.id:void 0,result:m}},N={broadcast:async(e,t)=>{const r={failed:0,outcomes:[],pruned:0,sent:0,total:0};let n=t?.after;const a=t?.limit;if(a!==void 0&&a<=0)return r;for(;;){const s=a===void 0?{...t,after:n}:{...t,after:n,limit:a-r.total},{nextCursor:c,result:i}=await E(e,s);if(r.failed+=i.failed,r.pruned+=i.pruned,r.sent+=i.sent,r.total+=i.total,r.outcomes.push(...i.outcomes),a!==void 0&&r.total>=a||c===void 0||c===n)break;n=c}return r},broadcastPage:E,list:e=>T(e),register:e=>("token"in e||I(),f.put(R(e,void 0,{allowedPushOrigins:o.allowedPushOrigins}))),send:async(e,t)=>{const{error:r,receipt:n}=await O(await _(e),t,!0);if(n===void 0)throw new v("INTERNAL",`@lunora/notify: push send failed: ${r??"unknown error"}`);return n},unregister:async(e,t)=>{await f.deleteOwned(e,t.userId??null)}},k=async(e,t)=>{if(h.getProvider(e)===void 0)throw A(e,"channel-not-configured"),new v("BAD_REQUEST",`@lunora/notify: the "${e}" channel is not configured in defineNotify(...)`);const r=await h.sendToChannel(e,t),n=r.successful?"accepted":"failed";return p(e,r.provider,n),n==="failed"&&y(e,r.provider,{error:S(r)}),r};return{notify:{chat:e=>k("chat",e),inApp:e=>k("inapp",e),push:N,send:async e=>{const t=await h.send(e);for(const r of t){const n=r.channel??"unknown",a=r.successful?"accepted":"failed";p(n,r.provider,a),a==="failed"&&y(n,r.provider,{error:S(r)})}return t},webhook:e=>k("webhook",e)},push:N}};export{Y as createNotify};
|