@lunora/notify 1.0.0-alpha.8 → 1.0.0-alpha.9
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/index.d.mts
CHANGED
|
@@ -114,6 +114,17 @@ interface SubscriptionFilter {
|
|
|
114
114
|
* `CreateNotifyOptions`'s `broadcastPageSize`, default 250) so a caller
|
|
115
115
|
* that sets `limit` to bound the audience doesn't also have to reason
|
|
116
116
|
* about page sizing.
|
|
117
|
+
*
|
|
118
|
+
* The non-positive sentinel means something different at each layer: at
|
|
119
|
+
* the STORE layer (`list`, and the `d1-store`/`memory-store` fetch-size
|
|
120
|
+
* hint) a non-positive `limit` means "no cap" — fetch everything. At the
|
|
121
|
+
* `broadcast`/`broadcastPage` layer, where `limit` is an AUDIENCE cap, a
|
|
122
|
+
* non-positive value instead means "no deliveries" — `broadcast({ limit: 0
|
|
123
|
+
* })` reaches nobody, not everybody. This asymmetry is deliberate: the two
|
|
124
|
+
* layers answer different questions ("how many rows to fetch" vs. "how
|
|
125
|
+
* many recipients to reach"), and unifying them would either break `list`
|
|
126
|
+
* callers relying on "no cap" or reintroduce the over-delivery this
|
|
127
|
+
* distinction fixes (see `broadcastPage`'s doc comment).
|
|
117
128
|
*/
|
|
118
129
|
limit?: number;
|
|
119
130
|
/** Restrict to a single owning user. */
|
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,17 @@ interface SubscriptionFilter {
|
|
|
114
114
|
* `CreateNotifyOptions`'s `broadcastPageSize`, default 250) so a caller
|
|
115
115
|
* that sets `limit` to bound the audience doesn't also have to reason
|
|
116
116
|
* about page sizing.
|
|
117
|
+
*
|
|
118
|
+
* The non-positive sentinel means something different at each layer: at
|
|
119
|
+
* the STORE layer (`list`, and the `d1-store`/`memory-store` fetch-size
|
|
120
|
+
* hint) a non-positive `limit` means "no cap" — fetch everything. At the
|
|
121
|
+
* `broadcast`/`broadcastPage` layer, where `limit` is an AUDIENCE cap, a
|
|
122
|
+
* non-positive value instead means "no deliveries" — `broadcast({ limit: 0
|
|
123
|
+
* })` reaches nobody, not everybody. This asymmetry is deliberate: the two
|
|
124
|
+
* layers answer different questions ("how many rows to fetch" vs. "how
|
|
125
|
+
* many recipients to reach"), and unifying them would either break `list`
|
|
126
|
+
* callers relying on "no cap" or reintroduce the over-delivery this
|
|
127
|
+
* distinction fixes (see `broadcastPage`'s doc comment).
|
|
117
128
|
*/
|
|
118
129
|
limit?: number;
|
|
119
130
|
/** Restrict to a single owning user. */
|
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-D0tvRTLQ.mjs";import{defineNotify as n,isNotifyDefinition as u}from"./packem_shared/defineNotify-DLEy53HJ.mjs";import{createNotify as p}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-D0tvRTLQ.mjs";import{defineNotify as n,isNotifyDefinition as u}from"./packem_shared/defineNotify-DLEy53HJ.mjs";import{createNotify as p}from"./packem_shared/createNotify-jNlehALj.mjs";import{buildEngine as d,routingPushProvider as x}from"./packem_shared/buildEngine-Jlzf2Jzn.mjs";import{enqueuePushBroadcast as c,runPushBroadcastJob as P}from"./packem_shared/enqueuePushBroadcast-tf1LqgzA.mjs";import{d1SubscriptionStore as b}from"./packem_shared/d1SubscriptionStore-67jpK0Vx.mjs";import{memorySubscriptionStore as N}from"./packem_shared/memorySubscriptionStore-FKZR4-fB.mjs";import{fcmId as g,isGoneError as y,normalizeRegisterInput as v,targetOf as B,webPushId as F}from"./packem_shared/fcmId-CFZ8IquF.mjs";export{e as FCM_ENV_KEYS,t as WEB_PUSH_ENV_KEYS,d as buildEngine,p as createNotify,b as d1SubscriptionStore,n as defineNotify,c as enqueuePushBroadcast,i as fcmFromEnv,g as fcmId,y as isGoneError,u as isNotifyDefinition,N as memorySubscriptionStore,v as normalizeRegisterInput,x as routingPushProvider,P as runPushBroadcastJob,B as targetOf,f as webPushFromEnv,F as webPushId};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as k}from"@lunora/errors";import{buildEngine as R}from"./buildEngine-Jlzf2Jzn.mjs";import{memorySubscriptionStore as W}from"./memorySubscriptionStore-FKZR4-fB.mjs";import{normalizeRegisterInput as Q,targetOf as U,isGoneError as j}from"./fcmId-CFZ8IquF.mjs";const z=250,g=(o,l)=>typeof o=="function"?o(l):o,b=o=>o.successful?void 0:o.errorMessages.join("; "),G=(o,l)=>o.successful?"accepted":j(l)?"gone":"failed",L=async(o,l,c)=>{const d=Array.from({length:o.length});let h=0;const p=async()=>{for(;h<o.length;){const f=h;h+=1,d[f]=await c(o[f])}};return await Promise.all(Array.from({length:Math.min(l,o.length)},()=>p())),d},_=(o,l)=>({chat:g(o.chat,l),fcm:g(o.fcm,l),inApp:g(o.inApp,l),webhook:g(o.webhook,l),webPush:g(o.webPush,l)}),E=new WeakMap,q=(o,l)=>{let c=E.get(o);c===void 0&&(c=new WeakMap,E.set(o,c));let d=c.get(l);return d===void 0&&(d={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},c.set(l,d)),d},V=(o,l,c={})=>{const d=q(o,l);let h;c.engine===void 0?(d.engine??=R(_(o,l)),h=d.engine):h=c.engine,d.store??=o.store?.(l);let{store:p}=d;p===void 0&&(d.fallbackStore??=W(),!c.silent&&!d.warnedNoStore&&(d.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.")),p=d.fallbackStore);const f=p,M=Math.max(1,c.concurrency??10),S=Math.max(1,c.broadcastPageSize??z),{log:T,metrics:P}=c,w=(t,e,r,i=1)=>{P?.count("notify.send",i,{channel:t,provider:e??t,status:r})},v=(t,e,r)=>{T?.warn(`notify ${t} delivery failed`,{channel:t,provider:e??t,status:"failed",...r})},N=(t,e)=>{P?.count("notify.skipped",1,{channel:t,reason:e})},$=()=>{const t=o.allowedPushOrigins!==void 0&&o.allowedPushOrigins.length>0;c.silent||t||d.warnedNoPushOriginAllowlist||(d.warnedNoPushOriginAllowlist=!0,console.warn("@lunora/notify: Web Push registered without `allowedPushOrigins` — the endpoint host is validated by a string classifier that does NOT resolve DNS, so a public hostname resolving to a private/internal IP (e.g. `https://127.0.0.1.nip.io/…`) is NOT blocked. Set `allowedPushOrigins` to the exact push-service origins to close DNS rebinding."))},I=async t=>(await f.list(t)).map(({keys:e,token:r,...i})=>i),C=async t=>{if(typeof t!="string")return t;const e=await f.get(t);if(e===void 0)throw new k("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${t}"`);return e},O=async(t,e,r)=>{let i,s,n;try{i=await h.sendToChannel("push",{...e,to:U(t)}),s=b(i),n=G(i,s)}catch(u){n="failed",s=u instanceof Error?u.message:String(u)}try{n==="accepted"?await f.markStatus(t.id,"ok"):n==="gone"?await f.delete(t.id):await f.markStatus(t.id,"failed",s)}catch{}return n==="failed"&&v("push",t.kind,{error:s,subscriptionId:t.id,userId:t.userId??null}),r&&w("push",t.kind,n),{error:s,receipt:i,status:n}},D=async(t,e)=>{const r=await L(e,M,async n=>{const{error:u,status:a}=await O(n,t,!1);return{error:u,kind:n.kind,status:a,subscription:n}}),i=new Map;for(const{kind:n,status:u}of r){const a=`${n} ${u}`,m=i.get(a);m===void 0?i.set(a,{count:1,kind:n,status:u}):m.count+=1}for(const{count:n,kind:u,status:a}of i.values())w("push",u,a,n);const s=r.map(({error:n,status:u,subscription:a})=>u==="accepted"?{id:a.id,status:"ok"}:u==="gone"?{error:n,id:a.id,status:"expired"}:{error:n,id:a.id,status:"failed"});return{failed:s.filter(n=>n.status==="failed").length,outcomes:s,pruned:s.filter(n=>n.status==="expired").length,sent:s.filter(n=>n.status==="ok").length,total:s.length}},A=async(t,e)=>{if(e?.limit!==void 0&&e.limit<=0)return{nextCursor:void 0,result:{failed:0,outcomes:[],pruned:0,sent:0,total:0}};const r=e?.limit!==void 0&&e.limit>0?Math.trunc(e.limit):void 0,i=r===void 0?S:Math.min(r,S),s=await f.list({after:e?.after,kind:e?.kind,limit:i+1,userId:e?.userId}),n=e?.after===void 0?s:s.filter(B=>B.id>e.after),u=n.length>i,a=u?n.slice(0,i):n;a.length===0&&e?.after===void 0&&N("push","no-subscriptions-matched");const m=await D(t,a);return{nextCursor:u?a[a.length-1]?.id:void 0,result:m}},x={broadcast:async(t,e)=>{const r={failed:0,outcomes:[],pruned:0,sent:0,total:0};let i=e?.after;const s=e?.limit;if(s!==void 0&&s<=0)return r;for(;;){const n=s===void 0?{...e,after:i}:{...e,after:i,limit:s-r.total},{nextCursor:u,result:a}=await A(t,n);if(r.failed+=a.failed,r.pruned+=a.pruned,r.sent+=a.sent,r.total+=a.total,r.outcomes.push(...a.outcomes),s!==void 0&&r.total>=s||u===void 0||u===i)break;i=u}return r},broadcastPage:A,list:t=>I(t),register:t=>("token"in t||$(),f.put(Q(t,void 0,{allowedPushOrigins:o.allowedPushOrigins}))),send:async(t,e)=>{const{error:r,receipt:i}=await O(await C(t),e,!0);if(i===void 0)throw new k("INTERNAL",`@lunora/notify: push send failed: ${r??"unknown error"}`);return i},unregister:t=>f.delete(t)},y=async(t,e)=>{if(h.getProvider(t)===void 0)throw N(t,"channel-not-configured"),new k("BAD_REQUEST",`@lunora/notify: the "${t}" channel is not configured in defineNotify(...)`);const r=await h.sendToChannel(t,e),i=r.successful?"accepted":"failed";return w(t,r.provider,i),i==="failed"&&v(t,r.provider,{error:b(r)}),r};return{notify:{chat:t=>y("chat",t),inApp:t=>y("inapp",t),push:x,send:async t=>{const e=await h.send(t);for(const r of e){const i=r.channel??"unknown",s=r.successful?"accepted":"failed";w(i,r.provider,s),s==="failed"&&v(i,r.provider,{error:b(r)})}return e},webhook:t=>y("webhook",t)},push:x}};export{V as createNotify};
|
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.9",
|
|
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,7 +49,7 @@
|
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
52
|
+
"@lunora/errors": "1.0.0-alpha.12",
|
|
53
53
|
"@visulima/notification": "1.0.5"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as k}from"@lunora/errors";import{buildEngine as R}from"./buildEngine-Jlzf2Jzn.mjs";import{memorySubscriptionStore as W}from"./memorySubscriptionStore-FKZR4-fB.mjs";import{normalizeRegisterInput as L,targetOf as Q,isGoneError as U}from"./fcmId-CFZ8IquF.mjs";const j=250,g=(s,u)=>typeof s=="function"?s(u):s,b=s=>s.successful?void 0:s.errorMessages.join("; "),z=(s,u)=>s.successful?"accepted":U(u)?"gone":"failed",G=async(s,u,d)=>{const c=Array.from({length:s.length});let h=0;const p=async()=>{for(;h<s.length;){const f=h;h+=1,c[f]=await d(s[f])}};return await Promise.all(Array.from({length:Math.min(u,s.length)},()=>p())),c},_=(s,u)=>({chat:g(s.chat,u),fcm:g(s.fcm,u),inApp:g(s.inApp,u),webhook:g(s.webhook,u),webPush:g(s.webPush,u)}),M=new WeakMap,q=(s,u)=>{let d=M.get(s);d===void 0&&(d=new WeakMap,M.set(s,d));let c=d.get(u);return c===void 0&&(c={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},d.set(u,c)),c},V=(s,u,d={})=>{const c=q(s,u);let h;d.engine===void 0?(c.engine??=R(_(s,u)),h=c.engine):h=d.engine,c.store??=s.store?.(u);let{store:p}=c;p===void 0&&(c.fallbackStore??=W(),!d.silent&&!c.warnedNoStore&&(c.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.")),p=c.fallbackStore);const f=p,T=Math.max(1,d.concurrency??10),S=Math.max(1,d.broadcastPageSize??j),{log:$,metrics:P}=d,w=(t,e,n,i=1)=>{P?.count("notify.send",i,{channel:t,provider:e??t,status:n})},v=(t,e,n)=>{$?.warn(`notify ${t} delivery failed`,{channel:t,provider:e??t,status:"failed",...n})},N=(t,e)=>{P?.count("notify.skipped",1,{channel:t,reason:e})},x=()=>{const t=s.allowedPushOrigins!==void 0&&s.allowedPushOrigins.length>0;d.silent||t||c.warnedNoPushOriginAllowlist||(c.warnedNoPushOriginAllowlist=!0,console.warn("@lunora/notify: Web Push registered without `allowedPushOrigins` — the endpoint host is validated by a string classifier that does NOT resolve DNS, so a public hostname resolving to a private/internal IP (e.g. `https://127.0.0.1.nip.io/…`) is NOT blocked. Set `allowedPushOrigins` to the exact push-service origins to close DNS rebinding."))},I=async t=>(await f.list(t)).map(({keys:e,token:n,...i})=>i),D=async t=>{if(typeof t!="string")return t;const e=await f.get(t);if(e===void 0)throw new k("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${t}"`);return e},O=async(t,e,n)=>{let i,o,r;try{i=await h.sendToChannel("push",{...e,to:Q(t)}),o=b(i),r=z(i,o)}catch(l){r="failed",o=l instanceof Error?l.message:String(l)}try{r==="accepted"?await f.markStatus(t.id,"ok"):r==="gone"?await f.delete(t.id):await f.markStatus(t.id,"failed",o)}catch{}return r==="failed"&&v("push",t.kind,{error:o,subscriptionId:t.id,userId:t.userId??null}),n&&w("push",t.kind,r),{error:o,receipt:i,status:r}},C=async(t,e)=>{const n=await G(e,T,async r=>{const{error:l,status:a}=await O(r,t,!1);return{error:l,kind:r.kind,status:a,subscription:r}}),i=new Map;for(const{kind:r,status:l}of n){const a=`${r} ${l}`,m=i.get(a);m===void 0?i.set(a,{count:1,kind:r,status:l}):m.count+=1}for(const{count:r,kind:l,status:a}of i.values())w("push",l,a,r);const o=n.map(({error:r,status:l,subscription:a})=>l==="accepted"?{id:a.id,status:"ok"}:l==="gone"?{error:r,id:a.id,status:"expired"}:{error:r,id:a.id,status:"failed"});return{failed:o.filter(r=>r.status==="failed").length,outcomes:o,pruned:o.filter(r=>r.status==="expired").length,sent:o.filter(r=>r.status==="ok").length,total:o.length}},A=async(t,e)=>{const n=e?.limit!==void 0&&e.limit>0?Math.trunc(e.limit):void 0,i=n===void 0?S:Math.min(n,S),o=await f.list({after:e?.after,kind:e?.kind,limit:i+1,userId:e?.userId}),r=e?.after===void 0?o:o.filter(B=>B.id>e.after),l=r.length>i,a=l?r.slice(0,i):r;a.length===0&&e?.after===void 0&&N("push","no-subscriptions-matched");const m=await C(t,a);return{nextCursor:l?a[a.length-1]?.id:void 0,result:m}},E={broadcast:async(t,e)=>{const n={failed:0,outcomes:[],pruned:0,sent:0,total:0};let i=e?.after;const o=e?.limit;for(;;){const r=o===void 0?{...e,after:i}:{...e,after:i,limit:o-n.total},{nextCursor:l,result:a}=await A(t,r);if(n.failed+=a.failed,n.pruned+=a.pruned,n.sent+=a.sent,n.total+=a.total,n.outcomes.push(...a.outcomes),o!==void 0&&n.total>=o||l===void 0||l===i)break;i=l}return n},broadcastPage:A,list:t=>I(t),register:t=>("token"in t||x(),f.put(L(t,void 0,{allowedPushOrigins:s.allowedPushOrigins}))),send:async(t,e)=>{const{error:n,receipt:i}=await O(await D(t),e,!0);if(i===void 0)throw new k("INTERNAL",`@lunora/notify: push send failed: ${n??"unknown error"}`);return i},unregister:t=>f.delete(t)},y=async(t,e)=>{if(h.getProvider(t)===void 0)throw N(t,"channel-not-configured"),new k("BAD_REQUEST",`@lunora/notify: the "${t}" channel is not configured in defineNotify(...)`);const n=await h.sendToChannel(t,e),i=n.successful?"accepted":"failed";return w(t,n.provider,i),i==="failed"&&v(t,n.provider,{error:b(n)}),n};return{notify:{chat:t=>y("chat",t),inApp:t=>y("inapp",t),push:E,send:async t=>{const e=await h.send(t);for(const n of e){const i=n.channel??"unknown",o=n.successful?"accepted":"failed";w(i,n.provider,o),o==="failed"&&v(i,n.provider,{error:b(n)})}return e},webhook:t=>y("webhook",t)},push:E}};export{V as createNotify};
|