@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-ClocmjsQ.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};
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.8",
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.11",
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};