@lunora/notify 1.0.0-alpha.79 → 1.0.0-alpha.80
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 +18 -22
- package/dist/index.d.ts +18 -22
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/createNotify-hH_Y5OJQ.mjs +1 -0
- package/dist/packem_shared/d1SubscriptionStore-D80r22Vc.mjs +1 -0
- package/dist/packem_shared/memorySubscriptionStore-Ccr3ErTV.mjs +1 -0
- package/package.json +1 -1
- package/dist/packem_shared/createNotify-DnO2QQ8p.mjs +0 -1
- package/dist/packem_shared/d1SubscriptionStore-h6gSRs-m.mjs +0 -1
- package/dist/packem_shared/memorySubscriptionStore-q8KD05cV.mjs +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -15,6 +15,15 @@ type NotifyEnv = Record<string, unknown>;
|
|
|
15
15
|
type SubscriptionKind = "fcm" | "web-push";
|
|
16
16
|
/** The last-known delivery outcome recorded on a subscription. */
|
|
17
17
|
type SubscriptionStatus = "expired" | "failed" | "ok";
|
|
18
|
+
/** The delivery outcomes of one broadcast page, grouped for {@link SubscriptionStore.applyOutcomes}. */
|
|
19
|
+
interface SubscriptionOutcomes {
|
|
20
|
+
failed: {
|
|
21
|
+
error?: string;
|
|
22
|
+
id: string;
|
|
23
|
+
}[];
|
|
24
|
+
gone: string[];
|
|
25
|
+
ok: string[];
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* A registered device/browser subscription. Web Push carries a W3C Push API
|
|
20
29
|
* `endpoint` + `keys`; FCM carries a device registration `token`. `id` is a
|
|
@@ -136,6 +145,13 @@ interface SubscriptionFilter {
|
|
|
136
145
|
* and a D1-backed store (durable, edge-safe).
|
|
137
146
|
*/
|
|
138
147
|
interface SubscriptionStore {
|
|
148
|
+
/**
|
|
149
|
+
* Apply a whole broadcast page's delivery outcomes at once (best-effort):
|
|
150
|
+
* `ok` ids are marked ok, `failed` ids marked failed with their error, `gone`
|
|
151
|
+
* ids deleted. OPTIONAL — when absent, `broadcast` calls `markStatus` /
|
|
152
|
+
* `delete` per id.
|
|
153
|
+
*/
|
|
154
|
+
applyOutcomes?: (outcomes: SubscriptionOutcomes) => Promise<void>;
|
|
139
155
|
/** Remove a subscription by id (idempotent). */
|
|
140
156
|
delete: (id: string) => Promise<void>;
|
|
141
157
|
/**
|
|
@@ -783,6 +799,7 @@ declare const runPushBroadcastPage: (push: LunoraPush, job: PushBroadcastJob) =>
|
|
|
783
799
|
* store runtime-agnostic and trivially fakeable in tests.
|
|
784
800
|
*/
|
|
785
801
|
interface D1Like {
|
|
802
|
+
batch: (statements: D1PreparedLike[]) => Promise<unknown>;
|
|
786
803
|
prepare: (query: string) => D1PreparedLike;
|
|
787
804
|
}
|
|
788
805
|
interface D1PreparedLike {
|
|
@@ -798,27 +815,6 @@ interface D1StoreOptions {
|
|
|
798
815
|
/** Table name (default `lunora_push_subscriptions`). Must be a bare identifier. */
|
|
799
816
|
tableName?: string;
|
|
800
817
|
}
|
|
801
|
-
/**
|
|
802
|
-
* A D1-backed {@link SubscriptionStore}. Edge-safe (D1 is a Worker binding). The
|
|
803
|
-
* backing table is created lazily on first use (`CREATE TABLE IF NOT EXISTS`), so
|
|
804
|
-
* no migration step is required for the subscription table itself.
|
|
805
|
-
*
|
|
806
|
-
* ID SCHEME / LAZY MIGRATION: `id` (the `PRIMARY KEY`, upserted via `ON
|
|
807
|
-
* CONFLICT(id) DO UPDATE`) is a version-prefixed digest of the endpoint/token —
|
|
808
|
-
* currently `wp2_`/`fcm2_` (64-bit FNV-1a; see `normalize.ts`). No table migration
|
|
809
|
-
* runs when the id scheme is revised: a returning device re-registers under its new
|
|
810
|
-
* id and upserts a fresh row, while its old-prefix row (`wp_`/`fcm_`) ages out via
|
|
811
|
-
* the normal gone-pruning on the next failed send. So a table can transiently hold
|
|
812
|
-
* both an old- and new-prefix row for one device — expected, self-healing, and the
|
|
813
|
-
* reason a prefix must NEVER be reused for a different scheme.
|
|
814
|
-
*
|
|
815
|
-
* ```ts
|
|
816
|
-
* export default defineNotify({
|
|
817
|
-
* webPush: (env) => webPushFromEnv(env),
|
|
818
|
-
* store: (env) => d1SubscriptionStore(env.DB),
|
|
819
|
-
* });
|
|
820
|
-
* ```
|
|
821
|
-
*/
|
|
822
818
|
declare const d1SubscriptionStore: (database: D1Like, options?: D1StoreOptions) => SubscriptionStore;
|
|
823
819
|
/**
|
|
824
820
|
* An in-memory {@link SubscriptionStore} — the zero-dependency default. Suitable
|
|
@@ -925,7 +921,7 @@ export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions,
|
|
|
925
921
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
|
926
922
|
* @packageDocumentation
|
|
927
923
|
*/
|
|
928
|
-
FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyDeliveryStatus, type NotifyEnv, type NotifyLogger, type NotifyMetrics, type NotifySkipReason, type PushBroadcastJob, type PushBroadcastPageOutcome, type PushOwner, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore,
|
|
924
|
+
FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyDeliveryStatus, type NotifyEnv, type NotifyLogger, type NotifyMetrics, type NotifySkipReason, type PushBroadcastJob, type PushBroadcastPageOutcome, type PushOwner, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionOutcomes, type SubscriptionStatus, type SubscriptionStore,
|
|
929
925
|
/**
|
|
930
926
|
* `@lunora/notify`
|
|
931
927
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,15 @@ type NotifyEnv = Record<string, unknown>;
|
|
|
15
15
|
type SubscriptionKind = "fcm" | "web-push";
|
|
16
16
|
/** The last-known delivery outcome recorded on a subscription. */
|
|
17
17
|
type SubscriptionStatus = "expired" | "failed" | "ok";
|
|
18
|
+
/** The delivery outcomes of one broadcast page, grouped for {@link SubscriptionStore.applyOutcomes}. */
|
|
19
|
+
interface SubscriptionOutcomes {
|
|
20
|
+
failed: {
|
|
21
|
+
error?: string;
|
|
22
|
+
id: string;
|
|
23
|
+
}[];
|
|
24
|
+
gone: string[];
|
|
25
|
+
ok: string[];
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* A registered device/browser subscription. Web Push carries a W3C Push API
|
|
20
29
|
* `endpoint` + `keys`; FCM carries a device registration `token`. `id` is a
|
|
@@ -136,6 +145,13 @@ interface SubscriptionFilter {
|
|
|
136
145
|
* and a D1-backed store (durable, edge-safe).
|
|
137
146
|
*/
|
|
138
147
|
interface SubscriptionStore {
|
|
148
|
+
/**
|
|
149
|
+
* Apply a whole broadcast page's delivery outcomes at once (best-effort):
|
|
150
|
+
* `ok` ids are marked ok, `failed` ids marked failed with their error, `gone`
|
|
151
|
+
* ids deleted. OPTIONAL — when absent, `broadcast` calls `markStatus` /
|
|
152
|
+
* `delete` per id.
|
|
153
|
+
*/
|
|
154
|
+
applyOutcomes?: (outcomes: SubscriptionOutcomes) => Promise<void>;
|
|
139
155
|
/** Remove a subscription by id (idempotent). */
|
|
140
156
|
delete: (id: string) => Promise<void>;
|
|
141
157
|
/**
|
|
@@ -783,6 +799,7 @@ declare const runPushBroadcastPage: (push: LunoraPush, job: PushBroadcastJob) =>
|
|
|
783
799
|
* store runtime-agnostic and trivially fakeable in tests.
|
|
784
800
|
*/
|
|
785
801
|
interface D1Like {
|
|
802
|
+
batch: (statements: D1PreparedLike[]) => Promise<unknown>;
|
|
786
803
|
prepare: (query: string) => D1PreparedLike;
|
|
787
804
|
}
|
|
788
805
|
interface D1PreparedLike {
|
|
@@ -798,27 +815,6 @@ interface D1StoreOptions {
|
|
|
798
815
|
/** Table name (default `lunora_push_subscriptions`). Must be a bare identifier. */
|
|
799
816
|
tableName?: string;
|
|
800
817
|
}
|
|
801
|
-
/**
|
|
802
|
-
* A D1-backed {@link SubscriptionStore}. Edge-safe (D1 is a Worker binding). The
|
|
803
|
-
* backing table is created lazily on first use (`CREATE TABLE IF NOT EXISTS`), so
|
|
804
|
-
* no migration step is required for the subscription table itself.
|
|
805
|
-
*
|
|
806
|
-
* ID SCHEME / LAZY MIGRATION: `id` (the `PRIMARY KEY`, upserted via `ON
|
|
807
|
-
* CONFLICT(id) DO UPDATE`) is a version-prefixed digest of the endpoint/token —
|
|
808
|
-
* currently `wp2_`/`fcm2_` (64-bit FNV-1a; see `normalize.ts`). No table migration
|
|
809
|
-
* runs when the id scheme is revised: a returning device re-registers under its new
|
|
810
|
-
* id and upserts a fresh row, while its old-prefix row (`wp_`/`fcm_`) ages out via
|
|
811
|
-
* the normal gone-pruning on the next failed send. So a table can transiently hold
|
|
812
|
-
* both an old- and new-prefix row for one device — expected, self-healing, and the
|
|
813
|
-
* reason a prefix must NEVER be reused for a different scheme.
|
|
814
|
-
*
|
|
815
|
-
* ```ts
|
|
816
|
-
* export default defineNotify({
|
|
817
|
-
* webPush: (env) => webPushFromEnv(env),
|
|
818
|
-
* store: (env) => d1SubscriptionStore(env.DB),
|
|
819
|
-
* });
|
|
820
|
-
* ```
|
|
821
|
-
*/
|
|
822
818
|
declare const d1SubscriptionStore: (database: D1Like, options?: D1StoreOptions) => SubscriptionStore;
|
|
823
819
|
/**
|
|
824
820
|
* An in-memory {@link SubscriptionStore} — the zero-dependency default. Suitable
|
|
@@ -925,7 +921,7 @@ export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions,
|
|
|
925
921
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
|
926
922
|
* @packageDocumentation
|
|
927
923
|
*/
|
|
928
|
-
FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyDeliveryStatus, type NotifyEnv, type NotifyLogger, type NotifyMetrics, type NotifySkipReason, type PushBroadcastJob, type PushBroadcastPageOutcome, type PushOwner, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore,
|
|
924
|
+
FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyDeliveryStatus, type NotifyEnv, type NotifyLogger, type NotifyMetrics, type NotifySkipReason, type PushBroadcastJob, type PushBroadcastPageOutcome, type PushOwner, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionOutcomes, type SubscriptionStatus, type SubscriptionStore,
|
|
929
925
|
/**
|
|
930
926
|
* `@lunora/notify`
|
|
931
927
|
*
|
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-hH_Y5OJQ.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-D80r22Vc.mjs";import{memorySubscriptionStore as h}from"./packem_shared/memorySubscriptionStore-Ccr3ErTV.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 S}from"@lunora/errors";import{buildEngine as B,checkPushTtl as C,checkPushUrgency as I}from"./buildEngine-x7r_sr3M.mjs";import{memorySubscriptionStore as L}from"./memorySubscriptionStore-Ccr3ErTV.mjs";import{n as U,t as W,i as j}from"./normalize-B5B9QUol.mjs";const G=250,p=(s,c)=>typeof s=="function"?s(c):s,P=s=>s.successful?void 0:s.errorMessages.join("; "),Q=(s,c,d)=>s.successful?"accepted":j(c,d)?"gone":"failed",Z=async(s,c,d)=>{const u=Array.from({length:s.length});let g=0;const w=async()=>{for(;g<s.length;){const f=g;g+=1,u[f]=await d(s[f])}};return await Promise.all(Array.from({length:Math.min(c,s.length)},()=>w())),u},q=(s,c)=>({allowedPushOrigins:s.allowedPushOrigins,chat:p(s.chat,c),fcm:p(s.fcm,c),inApp:p(s.inApp,c),webhook:p(s.webhook,c),webPush:p(s.webPush,c)}),M=new WeakMap,H=(s,c)=>{let d=M.get(s);d===void 0&&(d=new WeakMap,M.set(s,d));let u=d.get(c);return u===void 0&&(u={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},d.set(c,u)),u},te=(s,c,d={})=>{const u=H(s,c);let g;d.engine===void 0?(u.engine??=B(q(s,c)),g=u.engine):g=d.engine,u.store??=s.store?.(c);let{store:w}=u;w===void 0&&(u.fallbackStore??=L(),!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.")),w=u.fallbackStore);const f=w,T=Math.max(1,d.concurrency??s.concurrency??10),b=Math.max(1,d.broadcastPageSize??s.broadcastPageSize??G),{log:_,metrics:A}=d,m=(e,t,o,n=1)=>{A?.count("notify.send",n,{channel:e,provider:t??e,status:o})},y=(e,t,o)=>{_?.warn(`notify ${e} delivery failed`,{channel:e,provider:t??e,status:"failed",...o})},O=(e,t)=>{A?.count("notify.skipped",1,{channel:e,reason:t})},D=()=>{const e=s.allowedPushOrigins!==void 0&&s.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."))},$=async e=>(await f.list(e)).map(({keys:o,token:n,...i})=>i),z=async e=>{if(typeof e!="string")return e;const t=await f.get(e);if(t===void 0)throw new S("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${e}"`);return t},E=async(e,t,{countInline:o,deferStatus:n})=>{let i,l,r;try{i=await g.sendToChannel("push",{...t,to:W(e)}),l=P(i),r=Q(i,l,e.kind)}catch(a){r="failed",l=a instanceof Error?a.message:String(a)}if(!n)try{r==="accepted"?await f.markStatus(e.id,"ok"):r==="gone"?await f.delete(e.id):await f.markStatus(e.id,"failed",l)}catch{}return r==="failed"&&y("push",e.kind,{error:l,subscriptionId:e.id,userId:e.userId??null}),o&&m("push",e.kind,r),{error:l,receipt:i,status:r}},F=async(e,t)=>{const o=await Z(t,T,async r=>{const{error:a,status:h}=await E(r,e,{countInline:!1,deferStatus:!0});return{error:a,kind:r.kind,status:h,subscription:r}}),n={failed:[],gone:[],ok:[]};for(const{error:r,status:a,subscription:h}of o)a==="accepted"?n.ok.push(h.id):a==="gone"?n.gone.push(h.id):n.failed.push({error:r,id:h.id});try{f.applyOutcomes===void 0?await Promise.all([...n.ok.map(r=>f.markStatus(r,"ok")),...n.failed.map(({error:r,id:a})=>f.markStatus(a,"failed",r)),...n.gone.map(r=>f.delete(r))]):await f.applyOutcomes(n)}catch{}const i=new Map;for(const{kind:r,status:a}of o){const h=`${r} ${a}`,v=i.get(h);v===void 0?i.set(h,{count:1,kind:r,status:a}):v.count+=1}for(const{count:r,kind:a,status:h}of i.values())m("push",a,h,r);const l=o.map(({error:r,status:a,subscription:h})=>a==="accepted"?{id:h.id,status:"ok"}:a==="gone"?{error:r,id:h.id,status:"expired"}:{error:r,id:h.id,status:"failed"});return{failed:l.filter(r=>r.status==="failed").length,outcomes:l,pruned:l.filter(r=>r.status==="expired").length,sent:l.filter(r=>r.status==="ok").length,total:l.length}},N=async(e,t)=>{if(C(e.ttl),I(e.urgency),t?.limit!==void 0&&t.limit<=0)return{nextCursor:void 0,result:{failed:0,outcomes:[],pruned:0,sent:0,total:0}};const o=t?.limit!==void 0&&t.limit>0?Math.trunc(t.limit):void 0,n=o===void 0?b:Math.min(o,b),i=await f.list({after:t?.after,kind:t?.kind,limit:n+1,userId:t?.userId}),l=t?.after===void 0?i:i.filter(R=>R.id>t.after),r=l.length>n,a=r?l.slice(0,n):l;a.length===0&&t?.after===void 0&&O("push","no-subscriptions-matched");const h=await F(e,a);return{nextCursor:r?a[a.length-1]?.id:void 0,result:h}},x={broadcast:async(e,t)=>{const o={failed:0,outcomes:[],pruned:0,sent:0,total:0};let n=t?.after;const i=t?.limit;if(i!==void 0&&i<=0)return o;for(;;){const l=i===void 0?{...t,after:n}:{...t,after:n,limit:i-o.total},{nextCursor:r,result:a}=await N(e,l);if(o.failed+=a.failed,o.pruned+=a.pruned,o.sent+=a.sent,o.total+=a.total,o.outcomes.push(...a.outcomes),i!==void 0&&o.total>=i||r===void 0||r===n)break;n=r}return o},broadcastPage:N,list:e=>$(e),register:e=>("token"in e||D(),f.put(U(e,void 0,{allowedPushOrigins:s.allowedPushOrigins}))),send:async(e,t)=>{C(t.ttl),I(t.urgency);const{error:o,receipt:n}=await E(await z(e),t,{countInline:!0,deferStatus:!1});if(n===void 0)throw new S("INTERNAL",`@lunora/notify: push send failed: ${o??"unknown error"}`);return n},unregister:async(e,t)=>{await f.deleteOwned(e,t.userId??null)}},k=async(e,t)=>{if(g.getProvider(e)===void 0)throw O(e,"channel-not-configured"),new S("BAD_REQUEST",`@lunora/notify: the "${e}" channel is not configured in defineNotify(...)`);const o=await g.sendToChannel(e,t),n=o.successful?"accepted":"failed";return m(e,o.provider,n),n==="failed"&&y(e,o.provider,{error:P(o)}),o};return{notify:{chat:e=>k("chat",e),inApp:e=>k("inapp",e),push:x,send:async e=>{const t=await g.send(e);for(const o of t){const n=o.channel??"unknown",i=o.successful?"accepted":"failed";m(n,o.provider,i),i==="failed"&&y(n,o.provider,{error:P(o)})}return t},webhook:e=>k("webhook",e)},push:x}};export{te as createNotify};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as O}from"@lunora/errors";import{c as $,l as L}from"./normalize-B5B9QUol.mjs";const k=e=>/^[A-Z_]\w*$/i.test(e),R=e=>{const s={createdAt:e.created_at,id:e.id,kind:e.kind,lastSeenAt:e.last_seen_at,userId:e.user_id};if(e.endpoint!==null&&(s.endpoint=e.endpoint),e.p256dh!==null&&e.auth!==null&&(s.keys={auth:e.auth,p256dh:e.p256dh}),e.token!==null&&(s.token=e.token),e.last_status!==null&&(s.lastStatus=e.last_status),e.last_error!==null&&(s.lastError=e.last_error),e.metadata!==null)try{s.metadata=JSON.parse(e.metadata)}catch{}return s},c=90,F=(e,s={})=>{const n=s.tableName??"lunora_push_subscriptions";if(!k(n))throw new O("BAD_REQUEST",`@lunora/notify: d1SubscriptionStore tableName "${n}" is not a bare SQL identifier`);let T;const E=()=>(T===void 0&&(T=e.prepare(`CREATE TABLE IF NOT EXISTS ${n} (id TEXT PRIMARY KEY, kind TEXT NOT NULL, endpoint TEXT, p256dh TEXT, auth TEXT, token TEXT, user_id TEXT, metadata TEXT, created_at INTEGER NOT NULL, last_seen_at INTEGER NOT NULL, last_status TEXT, last_error TEXT)`).run().then(()=>e.prepare(`CREATE INDEX IF NOT EXISTS ${n}_user_id_idx ON ${n} (user_id)`).run()).then(()=>e.prepare(`CREATE INDEX IF NOT EXISTS ${n}_kind_idx ON ${n} (kind)`).run()).then(()=>{}),T.catch(()=>{T=void 0})),T),S=async t=>{await E();const a=await e.prepare(`SELECT * FROM ${n} WHERE id = ?1`).bind(t).first();return a===null?void 0:R(a)};return{applyOutcomes:async({failed:t,gone:a,ok:r})=>{await E();const l=Date.now(),d=[],p=(i,u,h)=>{for(let o=0;o<h.length;o+=c){const N=h.slice(o,o+c),m=N.map((v,I)=>`?${(I+4).toString()}`).join(", ");d.push(e.prepare(`UPDATE ${n} SET last_status = ?1, last_error = ?2, last_seen_at = ?3 WHERE id IN (${m})`).bind(i,u,l,...N))}};p("ok",null,r);const _=new Map;for(const{error:i,id:u}of t)_.set(i??null,[..._.get(i??null)??[],u]);for(const[i,u]of _)p("failed",i,u);for(let i=0;i<a.length;i+=c){const u=a.slice(i,i+c);d.push(e.prepare(`DELETE FROM ${n} WHERE id IN (${u.map((h,o)=>`?${(o+1).toString()}`).join(", ")})`).bind(...u))}d.length>0&&await e.batch(d)},delete:async t=>{await E(),await e.prepare(`DELETE FROM ${n} WHERE id = ?1`).bind(t).run()},deleteOwned:async(t,a)=>(await E(),await(a===null?e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND user_id IS NULL RETURNING id`).bind(t):e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND user_id = ?2 RETURNING id`).bind(t,a)).first()!==null),get:S,list:async t=>{await E();const a=[],r=[];t?.kind!==void 0&&(r.push(t.kind),a.push(`kind = ?${r.length.toString()}`)),t?.userId!==void 0&&(t.userId===null?a.push("user_id IS NULL"):(r.push(t.userId),a.push(`user_id = ?${r.length.toString()}`))),t?.after!==void 0&&(r.push(t.after),a.push(`id > ?${r.length.toString()}`));const l=a.length===0?"":` WHERE ${a.join(" AND ")}`,d=" ORDER BY id ASC";let p="";t?.limit!==void 0&&t.limit>0&&(r.push(Math.trunc(t.limit)),p=` LIMIT ?${r.length.toString()}`);const{results:_}=await e.prepare(`SELECT * FROM ${n}${l}${d}${p}`).bind(...r).all();return _.map(i=>R(i))},markStatus:async(t,a,r)=>{await E(),await e.prepare(`UPDATE ${n} SET last_status = ?2, last_error = ?3, last_seen_at = ?4 WHERE id = ?1`).bind(t,a,r??null,Date.now()).run()},put:async t=>{await E(),await e.prepare(`INSERT INTO ${n} (id, kind, endpoint, p256dh, auth, token, user_id, metadata, created_at, last_seen_at, last_status, last_error) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) ON CONFLICT(id) DO UPDATE SET kind = ?2, endpoint = ?3, p256dh = ?4, auth = ?5, token = ?6, user_id = ?7, metadata = ?8, last_seen_at = ?10 WHERE ${n}.user_id IS NULL OR ${n}.user_id = ?7`).bind(t.id,t.kind,t.endpoint??null,t.keys?.p256dh??null,t.keys?.auth??null,t.token??null,t.userId??null,t.metadata===void 0?null:JSON.stringify(t.metadata),t.createdAt,t.lastSeenAt,t.lastStatus??null,t.lastError??null).run();const a=await S(t.id),r=$(a,t);if(r!==void 0)throw r;const l=L(t);if(l!==void 0&&l!==t.id){const d=t.userId??null;await(d===null?e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND user_id IS NULL`).bind(l):e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND (user_id IS NULL OR user_id = ?2)`).bind(l,d)).run()}return a??t}}};export{F as d1SubscriptionStore};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{c as a,l}from"./normalize-B5B9QUol.mjs";const u=(t,e)=>t.id<e.id?-1:t.id>e.id?1:0,c=(t,e)=>e===void 0?!0:!(e.kind!==void 0&&t.kind!==e.kind||e.userId!==void 0&&(t.userId??null)!==e.userId),m=()=>{const t=new Map;return{delete:e=>(t.delete(e),Promise.resolve()),deleteOwned:(e,r)=>{const s=t.get(e);return s===void 0||(s.userId??null)!==r?Promise.resolve(!1):(t.delete(e),Promise.resolve(!0))},get:e=>Promise.resolve(t.get(e)),list:e=>{const r=[];for(const o of t.values())c(o,e)&&r.push(o);r.sort(u);const s=e?.after===void 0?r:r.filter(o=>o.id>e.after),d=e?.limit!==void 0&&e.limit>0?s.slice(0,Math.trunc(e.limit)):s;return Promise.resolve(d)},applyOutcomes:({failed:e,gone:r,ok:s})=>{const d=Date.now();for(const o of s){const i=t.get(o);i!==void 0&&t.set(o,{...i,lastError:void 0,lastSeenAt:d,lastStatus:"ok"})}for(const{error:o,id:i}of e){const n=t.get(i);n!==void 0&&t.set(i,{...n,lastError:o,lastSeenAt:d,lastStatus:"failed"})}for(const o of r)t.delete(o);return Promise.resolve()},markStatus:(e,r,s)=>{const d=t.get(e);return d!==void 0&&t.set(e,{...d,lastError:s,lastSeenAt:Date.now(),lastStatus:r}),Promise.resolve()},put:e=>{const r=t.get(e.id),s=a(r,e);if(s!==void 0)return Promise.reject(s);const d=r===void 0?e:{...r,...e,createdAt:r.createdAt};t.set(d.id,d);const o=l(e),i=o===void 0||o===e.id?void 0:t.get(o);return i!==void 0&&a(i,e)===void 0&&t.delete(i.id),Promise.resolve(d)}}};export{m as memorySubscriptionStore};
|
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.80",
|
|
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",
|
|
@@ -1 +0,0 @@
|
|
|
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};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as h}from"@lunora/errors";import{c as S,l as N}from"./normalize-B5B9QUol.mjs";const R=e=>/^[A-Z_]\w*$/i.test(e),T=e=>{const i={createdAt:e.created_at,id:e.id,kind:e.kind,lastSeenAt:e.last_seen_at,userId:e.user_id};if(e.endpoint!==null&&(i.endpoint=e.endpoint),e.p256dh!==null&&e.auth!==null&&(i.keys={auth:e.auth,p256dh:e.p256dh}),e.token!==null&&(i.token=e.token),e.last_status!==null&&(i.lastStatus=e.last_status),e.last_error!==null&&(i.lastError=e.last_error),e.metadata!==null)try{i.metadata=JSON.parse(e.metadata)}catch{}return i},A=(e,i={})=>{const n=i.tableName??"lunora_push_subscriptions";if(!R(n))throw new h("BAD_REQUEST",`@lunora/notify: d1SubscriptionStore tableName "${n}" is not a bare SQL identifier`);let s;const r=()=>(s===void 0&&(s=e.prepare(`CREATE TABLE IF NOT EXISTS ${n} (id TEXT PRIMARY KEY, kind TEXT NOT NULL, endpoint TEXT, p256dh TEXT, auth TEXT, token TEXT, user_id TEXT, metadata TEXT, created_at INTEGER NOT NULL, last_seen_at INTEGER NOT NULL, last_status TEXT, last_error TEXT)`).run().then(()=>e.prepare(`CREATE INDEX IF NOT EXISTS ${n}_user_id_idx ON ${n} (user_id)`).run()).then(()=>e.prepare(`CREATE INDEX IF NOT EXISTS ${n}_kind_idx ON ${n} (kind)`).run()).then(()=>{}),s.catch(()=>{s=void 0})),s),E=async t=>{await r();const a=await e.prepare(`SELECT * FROM ${n} WHERE id = ?1`).bind(t).first();return a===null?void 0:T(a)};return{delete:async t=>{await r(),await e.prepare(`DELETE FROM ${n} WHERE id = ?1`).bind(t).run()},deleteOwned:async(t,a)=>(await r(),await(a===null?e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND user_id IS NULL RETURNING id`).bind(t):e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND user_id = ?2 RETURNING id`).bind(t,a)).first()!==null),get:E,list:async t=>{await r();const a=[],d=[];t?.kind!==void 0&&(d.push(t.kind),a.push(`kind = ?${d.length.toString()}`)),t?.userId!==void 0&&(t.userId===null?a.push("user_id IS NULL"):(d.push(t.userId),a.push(`user_id = ?${d.length.toString()}`))),t?.after!==void 0&&(d.push(t.after),a.push(`id > ?${d.length.toString()}`));const l=a.length===0?"":` WHERE ${a.join(" AND ")}`,u=" ORDER BY id ASC";let o="";t?.limit!==void 0&&t.limit>0&&(d.push(Math.trunc(t.limit)),o=` LIMIT ?${d.length.toString()}`);const{results:_}=await e.prepare(`SELECT * FROM ${n}${l}${u}${o}`).bind(...d).all();return _.map(p=>T(p))},markStatus:async(t,a,d)=>{await r(),await e.prepare(`UPDATE ${n} SET last_status = ?2, last_error = ?3, last_seen_at = ?4 WHERE id = ?1`).bind(t,a,d??null,Date.now()).run()},put:async t=>{await r(),await e.prepare(`INSERT INTO ${n} (id, kind, endpoint, p256dh, auth, token, user_id, metadata, created_at, last_seen_at, last_status, last_error) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) ON CONFLICT(id) DO UPDATE SET kind = ?2, endpoint = ?3, p256dh = ?4, auth = ?5, token = ?6, user_id = ?7, metadata = ?8, last_seen_at = ?10 WHERE ${n}.user_id IS NULL OR ${n}.user_id = ?7`).bind(t.id,t.kind,t.endpoint??null,t.keys?.p256dh??null,t.keys?.auth??null,t.token??null,t.userId??null,t.metadata===void 0?null:JSON.stringify(t.metadata),t.createdAt,t.lastSeenAt,t.lastStatus??null,t.lastError??null).run();const a=await E(t.id),d=S(a,t);if(d!==void 0)throw d;const l=N(t);if(l!==void 0&&l!==t.id){const u=t.userId??null;await(u===null?e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND user_id IS NULL`).bind(l):e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND (user_id IS NULL OR user_id = ?2)`).bind(l,u)).run()}return a??t}}};export{A as d1SubscriptionStore};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{c as n,l as a}from"./normalize-B5B9QUol.mjs";const l=(t,e)=>t.id<e.id?-1:t.id>e.id?1:0,u=(t,e)=>e===void 0?!0:!(e.kind!==void 0&&t.kind!==e.kind||e.userId!==void 0&&(t.userId??null)!==e.userId),m=()=>{const t=new Map;return{delete:e=>(t.delete(e),Promise.resolve()),deleteOwned:(e,r)=>{const o=t.get(e);return o===void 0||(o.userId??null)!==r?Promise.resolve(!1):(t.delete(e),Promise.resolve(!0))},get:e=>Promise.resolve(t.get(e)),list:e=>{const r=[];for(const d of t.values())u(d,e)&&r.push(d);r.sort(l);const o=e?.after===void 0?r:r.filter(d=>d.id>e.after),s=e?.limit!==void 0&&e.limit>0?o.slice(0,Math.trunc(e.limit)):o;return Promise.resolve(s)},markStatus:(e,r,o)=>{const s=t.get(e);return s!==void 0&&t.set(e,{...s,lastError:o,lastSeenAt:Date.now(),lastStatus:r}),Promise.resolve()},put:e=>{const r=t.get(e.id),o=n(r,e);if(o!==void 0)return Promise.reject(o);const s=r===void 0?e:{...r,...e,createdAt:r.createdAt};t.set(s.id,s);const d=a(e),i=d===void 0||d===e.id?void 0:t.get(d);return i!==void 0&&n(i,e)===void 0&&t.delete(i.id),Promise.resolve(s)}}};export{m as memorySubscriptionStore};
|