@lunora/notify 1.0.0-alpha.29 → 1.0.0-alpha.30
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
CHANGED
|
@@ -91,7 +91,26 @@ Move a large broadcast off the request path with `@lunora/queue`:
|
|
|
91
91
|
await enqueuePushBroadcast(ctx.queues.push, { payload: { title: "New drop", body: "…" } });
|
|
92
92
|
|
|
93
93
|
// consumer (lunora/queues.ts)
|
|
94
|
-
for (const message of batch.messages)
|
|
94
|
+
for (const message of batch.messages) {
|
|
95
|
+
const { failedIds, nextCursor } = await runPushBroadcastPage(ctx.push, message.body);
|
|
96
|
+
|
|
97
|
+
// One message = ONE bounded page. Discarding `nextCursor` delivers only the
|
|
98
|
+
// first page (default 250 devices) and reports success for the whole audience.
|
|
99
|
+
if (nextCursor !== undefined) {
|
|
100
|
+
await enqueuePushBroadcast(ctx.queues.push, {
|
|
101
|
+
payload: message.body.payload,
|
|
102
|
+
filter: { ...message.body.filter, after: nextCursor },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Redeliver ONLY the recipients that failed — a retry of the whole page would
|
|
107
|
+
// re-POST everyone it already reached.
|
|
108
|
+
if (failedIds.length > 0) {
|
|
109
|
+
await enqueuePushBroadcast(ctx.queues.push, { payload: message.body.payload, retryIds: failedIds });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
message.ack();
|
|
113
|
+
}
|
|
95
114
|
```
|
|
96
115
|
|
|
97
116
|
## Subscription storage
|
package/dist/index.d.mts
CHANGED
|
@@ -247,7 +247,7 @@ interface LunoraPush {
|
|
|
247
247
|
* materialized wholesale in the isolate — see `CreateNotifyOptions`'s
|
|
248
248
|
* `broadcastPageSize`. This call still processes the WHOLE matched
|
|
249
249
|
* audience in one request/queue message; use {@link LunoraPush.broadcastPage}
|
|
250
|
-
* directly (as `
|
|
250
|
+
* directly (as `runPushBroadcastPage` does) to bound a single queue message
|
|
251
251
|
* to one page.
|
|
252
252
|
*/
|
|
253
253
|
broadcast: (payload: PushContent, filter?: SubscriptionFilter) => Promise<BroadcastResult>;
|
|
@@ -258,7 +258,7 @@ interface LunoraPush {
|
|
|
258
258
|
* {@link LunoraPush.broadcast} (retry/circuit-breaker, gone-pruning) but
|
|
259
259
|
* scoped to a single page; returns the page's own {@link BroadcastResult}
|
|
260
260
|
* plus a `nextCursor` to fetch the next page (`undefined` when done).
|
|
261
|
-
* Backs `
|
|
261
|
+
* Backs `runPushBroadcastPage` so one queue message does bounded work
|
|
262
262
|
* regardless of audience size — most app code should call
|
|
263
263
|
* {@link LunoraPush.broadcast} instead.
|
|
264
264
|
*/
|
|
@@ -419,7 +419,7 @@ interface CreateNotifyOptions {
|
|
|
419
419
|
* Each page is fetched, delivered, and counted independently before the
|
|
420
420
|
* next page's store round trip, so a huge audience is never materialized
|
|
421
421
|
* wholesale in the isolate. Also the per-message bound `push.broadcastPage`
|
|
422
|
-
* (and so `
|
|
422
|
+
* (and so `runPushBroadcastPage`) uses. A test/tuning seam — most apps never
|
|
423
423
|
* need to set this.
|
|
424
424
|
*/
|
|
425
425
|
broadcastPageSize?: number;
|
|
@@ -505,23 +505,45 @@ declare const buildEngine: (resolved: ResolvedProviders) => Notification;
|
|
|
505
505
|
* fan-out. Shaped to travel through a `@lunora/queue` producer/consumer without
|
|
506
506
|
* `@lunora/notify` depending on `@lunora/queue` (the seam stays structural).
|
|
507
507
|
* `filter.after`, when set, resumes a broadcast partway through (see
|
|
508
|
-
* {@link
|
|
508
|
+
* {@link runPushBroadcastPage}'s continuation semantics).
|
|
509
509
|
*/
|
|
510
510
|
interface PushBroadcastJob {
|
|
511
511
|
/** Subscription filter (which devices/users to target; `filter.after` resumes a paged broadcast). */
|
|
512
512
|
filter?: SubscriptionFilter;
|
|
513
513
|
/** The push payload to deliver (the `to` target is derived per subscription). */
|
|
514
514
|
payload: PushContent;
|
|
515
|
+
/**
|
|
516
|
+
* Redeliver to exactly these subscription ids instead of walking a page —
|
|
517
|
+
* an earlier page's {@link PushBroadcastPageOutcome.failedIds}. Set by the
|
|
518
|
+
* consumer when it re-enqueues a page's transient failures; `filter` is
|
|
519
|
+
* ignored on such a job. See {@link runPushBroadcastPage}.
|
|
520
|
+
*/
|
|
521
|
+
retryIds?: string[];
|
|
515
522
|
/** Discriminator so a shared queue can multiplex message kinds. */
|
|
516
523
|
type: "lunora.push.broadcast";
|
|
517
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* One page's outcome plus the ids that need redelivering.
|
|
527
|
+
*
|
|
528
|
+
* The consumer MUST act on BOTH fields: `nextCursor` continues the broadcast and
|
|
529
|
+
* `failedIds` redelivers the recipients this page missed. Acking a message while
|
|
530
|
+
* ignoring either silently drops part of the audience.
|
|
531
|
+
*/
|
|
532
|
+
interface PushBroadcastPageOutcome extends BroadcastPageResult {
|
|
533
|
+
/**
|
|
534
|
+
* Subscriptions that failed transiently on this run (gone/pruned devices are
|
|
535
|
+
* NOT here — they are deleted, not retried). Re-enqueue a job carrying these
|
|
536
|
+
* as `retryIds` to redeliver to just them.
|
|
537
|
+
*/
|
|
538
|
+
failedIds: string[];
|
|
539
|
+
}
|
|
518
540
|
/** The structural slice of a `@lunora/queue` producer (`ctx.queues.<name>`) used here. */
|
|
519
541
|
interface QueueProducerLike {
|
|
520
542
|
send: (body: PushBroadcastJob) => Promise<void>;
|
|
521
543
|
}
|
|
522
544
|
/**
|
|
523
545
|
* Enqueue a fan-out broadcast for background delivery through a `@lunora/queue`
|
|
524
|
-
* queue instead of blocking the request. Pair with {@link
|
|
546
|
+
* queue instead of blocking the request. Pair with {@link runPushBroadcastPage} in
|
|
525
547
|
* the queue consumer — see its doc comment for how a large audience continues
|
|
526
548
|
* across MULTIPLE messages (one bounded page per message), not one.
|
|
527
549
|
*
|
|
@@ -532,7 +554,7 @@ interface QueueProducerLike {
|
|
|
532
554
|
* // in lunora/queues.ts consumer:
|
|
533
555
|
* export const push = defineQueue({ async handler(batch, ctx) {
|
|
534
556
|
* for (const message of batch.messages) {
|
|
535
|
-
* const { nextCursor } = await
|
|
557
|
+
* const { failedIds, nextCursor } = await runPushBroadcastPage(ctx.push, message.body);
|
|
536
558
|
*
|
|
537
559
|
* if (nextCursor !== undefined) {
|
|
538
560
|
* // More pages remain — enqueue the continuation. Each message still
|
|
@@ -542,6 +564,13 @@ interface QueueProducerLike {
|
|
|
542
564
|
* filter: { ...message.body.filter, after: nextCursor },
|
|
543
565
|
* });
|
|
544
566
|
* }
|
|
567
|
+
*
|
|
568
|
+
* if (failedIds.length > 0) {
|
|
569
|
+
* // Redeliver ONLY the recipients that failed — never the whole page.
|
|
570
|
+
* await enqueuePushBroadcast(ctx.queues.push, { payload: message.body.payload, retryIds: failedIds });
|
|
571
|
+
* }
|
|
572
|
+
*
|
|
573
|
+
* message.ack();
|
|
545
574
|
* }
|
|
546
575
|
* }});
|
|
547
576
|
* ```
|
|
@@ -553,37 +582,33 @@ declare const enqueuePushBroadcast: (queue: QueueProducerLike, job: Omit<PushBro
|
|
|
553
582
|
* reuses the engine's retry + circuit-breaker middleware and prunes gone
|
|
554
583
|
* subscriptions).
|
|
555
584
|
*
|
|
556
|
-
* RETRY / CONTINUATION SEMANTICS
|
|
557
|
-
* broadcast job used to process the WHOLE audience in one message, which could
|
|
558
|
-
* exceed Worker CPU/wall limits for a large audience and made a retry re-run
|
|
559
|
-
* everything):
|
|
585
|
+
* RETRY / CONTINUATION SEMANTICS:
|
|
560
586
|
*
|
|
561
|
-
* - A job
|
|
587
|
+
* - A job processes exactly ONE bounded page (see `CreateNotifyOptions`'s
|
|
562
588
|
* page-size option, default 250, or `job.filter.limit` when smaller),
|
|
563
589
|
* keyset-paginated on the subscription `id` (see `SubscriptionFilter.after`)
|
|
564
590
|
* — so per-message work is bounded regardless of total audience size.
|
|
565
|
-
* -
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* -
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
declare const runPushBroadcastJob: (push: LunoraPush, job: PushBroadcastJob) => Promise<BroadcastPageResult>;
|
|
591
|
+
* - A page NEVER throws for a partial failure. Throwing discarded the page's
|
|
592
|
+
* `nextCursor`, which is the only way the broadcast advances: one device that
|
|
593
|
+
* fails permanently (a rotated VAPID keypair leaves a stale device answering
|
|
594
|
+
* `403 VapidPkHashMismatch` forever) would then stall the cursor, re-POST
|
|
595
|
+
* every already-delivered recipient on each retry, dead-letter, and leave
|
|
596
|
+
* every LATER page unreached. The page's `nextCursor` and its `failedIds`
|
|
597
|
+
* both come back instead.
|
|
598
|
+
* - The CALLER (the `lunora/queues.ts` consumer) re-enqueues: `filter.after:
|
|
599
|
+
* nextCursor` while more pages remain, and a `retryIds: failedIds` job when
|
|
600
|
+
* any recipient failed. `@lunora/notify` cannot do it itself — it has no
|
|
601
|
+
* `@lunora/queue` dependency (the seam stays structural) and no reference to
|
|
602
|
+
* the producer that enqueued this message. See the consumer example on
|
|
603
|
+
* {@link enqueuePushBroadcast}.
|
|
604
|
+
* - A `retryIds` job redelivers to exactly those ids and DOES throw while any
|
|
605
|
+
* of them still fails, so the queue's backoff/dead-letter bounds it. That
|
|
606
|
+
* message contains no already-delivered recipient, so nothing is re-sent.
|
|
607
|
+
* - Gone subscriptions (404/410, FCM `UNREGISTERED`) are pruned by the page and
|
|
608
|
+
* never appear in `failedIds` — an all-`pruned` page is a success, not a
|
|
609
|
+
* failure, as is an empty page.
|
|
610
|
+
*/
|
|
611
|
+
declare const runPushBroadcastPage: (push: LunoraPush, job: PushBroadcastJob) => Promise<PushBroadcastPageOutcome>;
|
|
587
612
|
/**
|
|
588
613
|
* The minimal structural slice of Cloudflare's `D1Database` this store uses. A
|
|
589
614
|
* structural type (rather than importing `@cloudflare/workers-types`) keeps the
|
|
@@ -702,13 +727,13 @@ export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions,
|
|
|
702
727
|
* Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
|
|
703
728
|
* Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
|
|
704
729
|
* deliberately **not** on the edge facade — route heavy fan-out through
|
|
705
|
-
* `@lunora/queue` (`enqueuePushBroadcast` / `
|
|
730
|
+
* `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastPage`).
|
|
706
731
|
*
|
|
707
732
|
* - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
|
|
708
733
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
|
709
734
|
* @packageDocumentation
|
|
710
735
|
*/
|
|
711
|
-
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 PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore,
|
|
736
|
+
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 PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore,
|
|
712
737
|
/**
|
|
713
738
|
* `@lunora/notify`
|
|
714
739
|
*
|
|
@@ -721,7 +746,7 @@ FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type No
|
|
|
721
746
|
* Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
|
|
722
747
|
* Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
|
|
723
748
|
* deliberately **not** on the edge facade — route heavy fan-out through
|
|
724
|
-
* `@lunora/queue` (`enqueuePushBroadcast` / `
|
|
749
|
+
* `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastPage`).
|
|
725
750
|
*
|
|
726
751
|
* - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
|
|
727
752
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
|
@@ -740,13 +765,13 @@ WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1Subsc
|
|
|
740
765
|
* Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
|
|
741
766
|
* Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
|
|
742
767
|
* deliberately **not** on the edge facade — route heavy fan-out through
|
|
743
|
-
* `@lunora/queue` (`enqueuePushBroadcast` / `
|
|
768
|
+
* `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastPage`).
|
|
744
769
|
*
|
|
745
770
|
* - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
|
|
746
771
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
|
747
772
|
* @packageDocumentation
|
|
748
773
|
*/
|
|
749
|
-
fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider,
|
|
774
|
+
fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastPage, targetOf,
|
|
750
775
|
/**
|
|
751
776
|
* `@lunora/notify`
|
|
752
777
|
*
|
|
@@ -759,7 +784,7 @@ fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, nor
|
|
|
759
784
|
* Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
|
|
760
785
|
* Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
|
|
761
786
|
* deliberately **not** on the edge facade — route heavy fan-out through
|
|
762
|
-
* `@lunora/queue` (`enqueuePushBroadcast` / `
|
|
787
|
+
* `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastPage`).
|
|
763
788
|
*
|
|
764
789
|
* - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
|
|
765
790
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
package/dist/index.d.ts
CHANGED
|
@@ -247,7 +247,7 @@ interface LunoraPush {
|
|
|
247
247
|
* materialized wholesale in the isolate — see `CreateNotifyOptions`'s
|
|
248
248
|
* `broadcastPageSize`. This call still processes the WHOLE matched
|
|
249
249
|
* audience in one request/queue message; use {@link LunoraPush.broadcastPage}
|
|
250
|
-
* directly (as `
|
|
250
|
+
* directly (as `runPushBroadcastPage` does) to bound a single queue message
|
|
251
251
|
* to one page.
|
|
252
252
|
*/
|
|
253
253
|
broadcast: (payload: PushContent, filter?: SubscriptionFilter) => Promise<BroadcastResult>;
|
|
@@ -258,7 +258,7 @@ interface LunoraPush {
|
|
|
258
258
|
* {@link LunoraPush.broadcast} (retry/circuit-breaker, gone-pruning) but
|
|
259
259
|
* scoped to a single page; returns the page's own {@link BroadcastResult}
|
|
260
260
|
* plus a `nextCursor` to fetch the next page (`undefined` when done).
|
|
261
|
-
* Backs `
|
|
261
|
+
* Backs `runPushBroadcastPage` so one queue message does bounded work
|
|
262
262
|
* regardless of audience size — most app code should call
|
|
263
263
|
* {@link LunoraPush.broadcast} instead.
|
|
264
264
|
*/
|
|
@@ -419,7 +419,7 @@ interface CreateNotifyOptions {
|
|
|
419
419
|
* Each page is fetched, delivered, and counted independently before the
|
|
420
420
|
* next page's store round trip, so a huge audience is never materialized
|
|
421
421
|
* wholesale in the isolate. Also the per-message bound `push.broadcastPage`
|
|
422
|
-
* (and so `
|
|
422
|
+
* (and so `runPushBroadcastPage`) uses. A test/tuning seam — most apps never
|
|
423
423
|
* need to set this.
|
|
424
424
|
*/
|
|
425
425
|
broadcastPageSize?: number;
|
|
@@ -505,23 +505,45 @@ declare const buildEngine: (resolved: ResolvedProviders) => Notification;
|
|
|
505
505
|
* fan-out. Shaped to travel through a `@lunora/queue` producer/consumer without
|
|
506
506
|
* `@lunora/notify` depending on `@lunora/queue` (the seam stays structural).
|
|
507
507
|
* `filter.after`, when set, resumes a broadcast partway through (see
|
|
508
|
-
* {@link
|
|
508
|
+
* {@link runPushBroadcastPage}'s continuation semantics).
|
|
509
509
|
*/
|
|
510
510
|
interface PushBroadcastJob {
|
|
511
511
|
/** Subscription filter (which devices/users to target; `filter.after` resumes a paged broadcast). */
|
|
512
512
|
filter?: SubscriptionFilter;
|
|
513
513
|
/** The push payload to deliver (the `to` target is derived per subscription). */
|
|
514
514
|
payload: PushContent;
|
|
515
|
+
/**
|
|
516
|
+
* Redeliver to exactly these subscription ids instead of walking a page —
|
|
517
|
+
* an earlier page's {@link PushBroadcastPageOutcome.failedIds}. Set by the
|
|
518
|
+
* consumer when it re-enqueues a page's transient failures; `filter` is
|
|
519
|
+
* ignored on such a job. See {@link runPushBroadcastPage}.
|
|
520
|
+
*/
|
|
521
|
+
retryIds?: string[];
|
|
515
522
|
/** Discriminator so a shared queue can multiplex message kinds. */
|
|
516
523
|
type: "lunora.push.broadcast";
|
|
517
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* One page's outcome plus the ids that need redelivering.
|
|
527
|
+
*
|
|
528
|
+
* The consumer MUST act on BOTH fields: `nextCursor` continues the broadcast and
|
|
529
|
+
* `failedIds` redelivers the recipients this page missed. Acking a message while
|
|
530
|
+
* ignoring either silently drops part of the audience.
|
|
531
|
+
*/
|
|
532
|
+
interface PushBroadcastPageOutcome extends BroadcastPageResult {
|
|
533
|
+
/**
|
|
534
|
+
* Subscriptions that failed transiently on this run (gone/pruned devices are
|
|
535
|
+
* NOT here — they are deleted, not retried). Re-enqueue a job carrying these
|
|
536
|
+
* as `retryIds` to redeliver to just them.
|
|
537
|
+
*/
|
|
538
|
+
failedIds: string[];
|
|
539
|
+
}
|
|
518
540
|
/** The structural slice of a `@lunora/queue` producer (`ctx.queues.<name>`) used here. */
|
|
519
541
|
interface QueueProducerLike {
|
|
520
542
|
send: (body: PushBroadcastJob) => Promise<void>;
|
|
521
543
|
}
|
|
522
544
|
/**
|
|
523
545
|
* Enqueue a fan-out broadcast for background delivery through a `@lunora/queue`
|
|
524
|
-
* queue instead of blocking the request. Pair with {@link
|
|
546
|
+
* queue instead of blocking the request. Pair with {@link runPushBroadcastPage} in
|
|
525
547
|
* the queue consumer — see its doc comment for how a large audience continues
|
|
526
548
|
* across MULTIPLE messages (one bounded page per message), not one.
|
|
527
549
|
*
|
|
@@ -532,7 +554,7 @@ interface QueueProducerLike {
|
|
|
532
554
|
* // in lunora/queues.ts consumer:
|
|
533
555
|
* export const push = defineQueue({ async handler(batch, ctx) {
|
|
534
556
|
* for (const message of batch.messages) {
|
|
535
|
-
* const { nextCursor } = await
|
|
557
|
+
* const { failedIds, nextCursor } = await runPushBroadcastPage(ctx.push, message.body);
|
|
536
558
|
*
|
|
537
559
|
* if (nextCursor !== undefined) {
|
|
538
560
|
* // More pages remain — enqueue the continuation. Each message still
|
|
@@ -542,6 +564,13 @@ interface QueueProducerLike {
|
|
|
542
564
|
* filter: { ...message.body.filter, after: nextCursor },
|
|
543
565
|
* });
|
|
544
566
|
* }
|
|
567
|
+
*
|
|
568
|
+
* if (failedIds.length > 0) {
|
|
569
|
+
* // Redeliver ONLY the recipients that failed — never the whole page.
|
|
570
|
+
* await enqueuePushBroadcast(ctx.queues.push, { payload: message.body.payload, retryIds: failedIds });
|
|
571
|
+
* }
|
|
572
|
+
*
|
|
573
|
+
* message.ack();
|
|
545
574
|
* }
|
|
546
575
|
* }});
|
|
547
576
|
* ```
|
|
@@ -553,37 +582,33 @@ declare const enqueuePushBroadcast: (queue: QueueProducerLike, job: Omit<PushBro
|
|
|
553
582
|
* reuses the engine's retry + circuit-breaker middleware and prunes gone
|
|
554
583
|
* subscriptions).
|
|
555
584
|
*
|
|
556
|
-
* RETRY / CONTINUATION SEMANTICS
|
|
557
|
-
* broadcast job used to process the WHOLE audience in one message, which could
|
|
558
|
-
* exceed Worker CPU/wall limits for a large audience and made a retry re-run
|
|
559
|
-
* everything):
|
|
585
|
+
* RETRY / CONTINUATION SEMANTICS:
|
|
560
586
|
*
|
|
561
|
-
* - A job
|
|
587
|
+
* - A job processes exactly ONE bounded page (see `CreateNotifyOptions`'s
|
|
562
588
|
* page-size option, default 250, or `job.filter.limit` when smaller),
|
|
563
589
|
* keyset-paginated on the subscription `id` (see `SubscriptionFilter.after`)
|
|
564
590
|
* — so per-message work is bounded regardless of total audience size.
|
|
565
|
-
* -
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* -
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
declare const runPushBroadcastJob: (push: LunoraPush, job: PushBroadcastJob) => Promise<BroadcastPageResult>;
|
|
591
|
+
* - A page NEVER throws for a partial failure. Throwing discarded the page's
|
|
592
|
+
* `nextCursor`, which is the only way the broadcast advances: one device that
|
|
593
|
+
* fails permanently (a rotated VAPID keypair leaves a stale device answering
|
|
594
|
+
* `403 VapidPkHashMismatch` forever) would then stall the cursor, re-POST
|
|
595
|
+
* every already-delivered recipient on each retry, dead-letter, and leave
|
|
596
|
+
* every LATER page unreached. The page's `nextCursor` and its `failedIds`
|
|
597
|
+
* both come back instead.
|
|
598
|
+
* - The CALLER (the `lunora/queues.ts` consumer) re-enqueues: `filter.after:
|
|
599
|
+
* nextCursor` while more pages remain, and a `retryIds: failedIds` job when
|
|
600
|
+
* any recipient failed. `@lunora/notify` cannot do it itself — it has no
|
|
601
|
+
* `@lunora/queue` dependency (the seam stays structural) and no reference to
|
|
602
|
+
* the producer that enqueued this message. See the consumer example on
|
|
603
|
+
* {@link enqueuePushBroadcast}.
|
|
604
|
+
* - A `retryIds` job redelivers to exactly those ids and DOES throw while any
|
|
605
|
+
* of them still fails, so the queue's backoff/dead-letter bounds it. That
|
|
606
|
+
* message contains no already-delivered recipient, so nothing is re-sent.
|
|
607
|
+
* - Gone subscriptions (404/410, FCM `UNREGISTERED`) are pruned by the page and
|
|
608
|
+
* never appear in `failedIds` — an all-`pruned` page is a success, not a
|
|
609
|
+
* failure, as is an empty page.
|
|
610
|
+
*/
|
|
611
|
+
declare const runPushBroadcastPage: (push: LunoraPush, job: PushBroadcastJob) => Promise<PushBroadcastPageOutcome>;
|
|
587
612
|
/**
|
|
588
613
|
* The minimal structural slice of Cloudflare's `D1Database` this store uses. A
|
|
589
614
|
* structural type (rather than importing `@cloudflare/workers-types`) keeps the
|
|
@@ -702,13 +727,13 @@ export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions,
|
|
|
702
727
|
* Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
|
|
703
728
|
* Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
|
|
704
729
|
* deliberately **not** on the edge facade — route heavy fan-out through
|
|
705
|
-
* `@lunora/queue` (`enqueuePushBroadcast` / `
|
|
730
|
+
* `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastPage`).
|
|
706
731
|
*
|
|
707
732
|
* - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
|
|
708
733
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
|
709
734
|
* @packageDocumentation
|
|
710
735
|
*/
|
|
711
|
-
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 PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore,
|
|
736
|
+
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 PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore,
|
|
712
737
|
/**
|
|
713
738
|
* `@lunora/notify`
|
|
714
739
|
*
|
|
@@ -721,7 +746,7 @@ FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type No
|
|
|
721
746
|
* Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
|
|
722
747
|
* Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
|
|
723
748
|
* deliberately **not** on the edge facade — route heavy fan-out through
|
|
724
|
-
* `@lunora/queue` (`enqueuePushBroadcast` / `
|
|
749
|
+
* `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastPage`).
|
|
725
750
|
*
|
|
726
751
|
* - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
|
|
727
752
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
|
@@ -740,13 +765,13 @@ WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1Subsc
|
|
|
740
765
|
* Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
|
|
741
766
|
* Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
|
|
742
767
|
* deliberately **not** on the edge facade — route heavy fan-out through
|
|
743
|
-
* `@lunora/queue` (`enqueuePushBroadcast` / `
|
|
768
|
+
* `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastPage`).
|
|
744
769
|
*
|
|
745
770
|
* - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
|
|
746
771
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
|
747
772
|
* @packageDocumentation
|
|
748
773
|
*/
|
|
749
|
-
fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider,
|
|
774
|
+
fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastPage, targetOf,
|
|
750
775
|
/**
|
|
751
776
|
* `@lunora/notify`
|
|
752
777
|
*
|
|
@@ -759,7 +784,7 @@ fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, nor
|
|
|
759
784
|
* Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
|
|
760
785
|
* Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
|
|
761
786
|
* deliberately **not** on the edge facade — route heavy fan-out through
|
|
762
|
-
* `@lunora/queue` (`enqueuePushBroadcast` / `
|
|
787
|
+
* `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastPage`).
|
|
763
788
|
*
|
|
764
789
|
* - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
|
|
765
790
|
* - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
|
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 u}from"./packem_shared/defineNotify-CUi2k7pP.mjs";import{createNotify as p}from"./packem_shared/createNotify-BDE42tvH.mjs";import{buildEngine as
|
|
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 u}from"./packem_shared/defineNotify-CUi2k7pP.mjs";import{createNotify as p}from"./packem_shared/createNotify-BDE42tvH.mjs";import{buildEngine as a,routingPushProvider as d}from"./packem_shared/buildEngine-oDWs9Pom.mjs";import{enqueuePushBroadcast as P,runPushBroadcastPage as c}from"./packem_shared/enqueuePushBroadcast-B6v3DLLp.mjs";import{d1SubscriptionStore as b}from"./packem_shared/d1SubscriptionStore-s-AJH4hS.mjs";import{memorySubscriptionStore as h}from"./packem_shared/memorySubscriptionStore-FY8yANEM.mjs";import{fcmId as _,isGoneError as y,normalizeRegisterInput as v,targetOf as B,webPushId as F}from"./packem_shared/fcmId-Dwh_R5xe.mjs";export{e as FCM_ENV_KEYS,t as WEB_PUSH_ENV_KEYS,a as buildEngine,p as createNotify,b as d1SubscriptionStore,n as defineNotify,P as enqueuePushBroadcast,i as fcmFromEnv,_ as fcmId,y as isGoneError,u as isNotifyDefinition,h as memorySubscriptionStore,v as normalizeRegisterInput,d as routingPushProvider,c as runPushBroadcastPage,B as targetOf,f as webPushFromEnv,F as webPushId};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as i}from"@lunora/errors";const u=e=>e.filter(t=>t.status==="failed").map(t=>t.id),h=(e,t)=>e.send({...t,type:"lunora.push.broadcast"}),c=async(e,t,s)=>{const r=[];for(const a of s)try{const o=await e.send(a,t);r.push({id:a,status:o.successful?"ok":"failed"})}catch(o){r.push({error:o instanceof Error?o.message:String(o),id:a,status:"failed"})}const n=u(r),d=r.length-n.length,l={failed:n.length,outcomes:r,pruned:0,sent:d,total:r.length};if(n.length>0)throw new i("INTERNAL",`@lunora/notify: push retry failed for ${n.length.toString()} of ${r.length.toString()} subscription(s) — throwing so the queue retries and eventually dead-letters them`);return{failedIds:n,nextCursor:void 0,result:l}},g=async(e,t)=>{if(t.retryIds!==void 0&&t.retryIds.length>0)return c(e,t.payload,t.retryIds);const s=await e.broadcastPage(t.payload,t.filter);return{failedIds:u(s.result.outcomes),nextCursor:s.nextCursor,result:s.result}};export{h as enqueuePushBroadcast,g as runPushBroadcastPage};
|
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.30",
|
|
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 s}from"@lunora/errors";const o=(e,r)=>e.send({...r,type:"lunora.push.broadcast"}),n=async(e,r)=>{const t=await e.broadcastPage(r.payload,r.filter);if(t.result.failed>0)throw new s("INTERNAL",`@lunora/notify: push broadcast page had ${t.result.failed.toString()} transient failure(s) of ${t.result.total.toString()} subscription(s) (${t.result.sent.toString()} sent, ${t.result.pruned.toString()} pruned) — throwing so the queue retries this page`);return t};export{o as enqueuePushBroadcast,n as runPushBroadcastJob};
|