@lunora/notify 1.0.0-alpha.1 → 1.0.0-alpha.2
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
|
@@ -98,6 +98,19 @@ for (const message of batch.messages) await runPushBroadcastJob(ctx.push, messag
|
|
|
98
98
|
|
|
99
99
|
`SubscriptionStore` implementations: `memorySubscriptionStore()` (non-durable default, tests/dev) and `d1SubscriptionStore(db)` (durable, edge-safe, lazy table creation). Lifecycle: register (upsert), list/filter (by kind or user), status marking, and automatic prune of gone subscriptions on send/broadcast.
|
|
100
100
|
|
|
101
|
+
## Delivery observability
|
|
102
|
+
|
|
103
|
+
Every send is counted onto `ctx.metrics` and failures onto `ctx.log` for you — codegen threads the request's logger/metrics into `ctx.notify` (`createNotify(notifyConfig, env, { log, metrics })`), so there is nothing to wire. Two low-cardinality metric series feed the durable metric history + trend charts:
|
|
104
|
+
|
|
105
|
+
- **`notify.send`** `{ channel, provider, status }` — attempted sends. `status` is `accepted` (the provider took it), `failed`, or `gone` (endpoint unregistered — 404/410 / FCM `UNREGISTERED` — and pruned). A single send counts 1; a **broadcast aggregates** into one count per `(provider, status)` bucket (value = the bucket's count), not one per recipient — each `ctx.metrics.count` is a durable write.
|
|
106
|
+
- **`notify.skipped`** `{ channel, reason }` — a send that reached nobody: `no-subscriptions-matched` (empty broadcast) or `channel-not-configured`.
|
|
107
|
+
|
|
108
|
+
A **failed** send also emits one `ctx.log.warn` line carrying the error and, for push, the subscription/user ids — trace-correlated to the enclosing action and durably archived. Successes and prunes stay off the log; failure logs stay per-recipient even in a broadcast (they have no durable write).
|
|
109
|
+
|
|
110
|
+
`accepted` means the provider **accepted** the message, not that it was delivered or opened: Web Push and FCM give no delivery/open receipts, so the status stops at the send attempt. See [Observability → Delivery metrics](/docs/concepts/observability#delivery-metrics-notify).
|
|
111
|
+
|
|
101
112
|
## Status
|
|
102
113
|
|
|
103
|
-
|
|
114
|
+
Shipped: Web Push + FCM channels, chat / in-app / webhook senders, device-subscription storage (memory + D1), queue-backed fan-out, the codegen ctx-splice that auto-wires `ctx.notify` / `ctx.push` from `lunora/notify.ts` (via `createNotify`, mirroring `defineFlags` → `ctx.flags`), the `notify_send_outside_action` advisor lint, the Studio **Notifications** page (registered-device inspector), and [delivery observability](#delivery-observability).
|
|
115
|
+
|
|
116
|
+
Deferred: a filterable per-delivery **activity feed** and per-device history (a Novu-style drill-down). Web Push / FCM give no delivery/open receipts, so it would report only the send-attempt outcome; it needs a field-level predicate on the durable log reader (or a dedicated store) and is not planned until asked for.
|
package/dist/index.d.mts
CHANGED
|
@@ -119,6 +119,49 @@ interface BroadcastResult {
|
|
|
119
119
|
/** Total subscriptions attempted. */
|
|
120
120
|
total: number;
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The compact, stable delivery-status vocabulary emitted on notify observability
|
|
124
|
+
* signals — the `status` dimension on the `notify.send` metric and the failure
|
|
125
|
+
* log line. Modeled on Novu's execution status, but honest to edge push:
|
|
126
|
+
*
|
|
127
|
+
* - `accepted` — the provider took the message (a `Receipt.successful` send).
|
|
128
|
+
* - `failed` — a provider error; the log line carries the `error` text.
|
|
129
|
+
* - `gone` — the endpoint is unregistered (404/410, FCM `UNREGISTERED`) and pruned; push-only.
|
|
130
|
+
*
|
|
131
|
+
* Web Push and FCM give no delivery/open receipts, so the vocabulary stops at the
|
|
132
|
+
* send attempt: a `delivered`/`opened` status would be a lie for these channels.
|
|
133
|
+
* The one place a later `seen`/`read` is real is the in-app inbox, where the
|
|
134
|
+
* client posts a read receipt back — out of scope here.
|
|
135
|
+
*/
|
|
136
|
+
type NotifyDeliveryStatus = "accepted" | "failed" | "gone";
|
|
137
|
+
/**
|
|
138
|
+
* Why a send fanned out to nobody — the "sent 0 because…" signal (mirrors Novu's
|
|
139
|
+
* pre-send `DetailEnum` reasons). Emitted as the `reason` dimension on a
|
|
140
|
+
* `notify.skipped` metric so a no-op is visible in the Studio metric/trend view
|
|
141
|
+
* instead of silent.
|
|
142
|
+
*
|
|
143
|
+
* - `no-subscriptions-matched` — the store held no device for the broadcast filter.
|
|
144
|
+
* - `channel-not-configured` — the target channel was never wired in `defineNotify`.
|
|
145
|
+
*/
|
|
146
|
+
type NotifySkipReason = "channel-not-configured" | "no-subscriptions-matched";
|
|
147
|
+
/**
|
|
148
|
+
* The minimal structural slice of `ctx.log` the notify facade emits through — just
|
|
149
|
+
* the `warn` severity it uses for a failed delivery. Structural (rather than a
|
|
150
|
+
* dependency on `@lunora/server`'s `LunoraLogger`) so codegen passes the real
|
|
151
|
+
* `ctx.log` and a test passes a spy — the D1-store `D1Like` pattern, applied to
|
|
152
|
+
* observability.
|
|
153
|
+
*/
|
|
154
|
+
interface NotifyLogger {
|
|
155
|
+
warn: (message: string, fields?: Record<string, unknown>) => void;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The minimal structural slice of `ctx.metrics` the notify facade emits through —
|
|
159
|
+
* the `count` instrument backing the `notify.send` / `notify.skipped` series.
|
|
160
|
+
* Structural for the same reason as {@link NotifyLogger}.
|
|
161
|
+
*/
|
|
162
|
+
interface NotifyMetrics {
|
|
163
|
+
count: (name: string, value?: number, attributes?: Record<string, unknown>) => void;
|
|
164
|
+
}
|
|
122
165
|
/**
|
|
123
166
|
* The push sub-facade — spliced onto ctx as `ctx.push` (and reachable as
|
|
124
167
|
* `ctx.notify.push`). Owns the device-subscription lifecycle plus targeted and
|
|
@@ -268,6 +311,22 @@ interface CreateNotifyOptions {
|
|
|
268
311
|
* config resolution entirely.
|
|
269
312
|
*/
|
|
270
313
|
engine?: Notification;
|
|
314
|
+
/**
|
|
315
|
+
* The request's `ctx.log` (structural {@link NotifyLogger}). Codegen threads
|
|
316
|
+
* `ctx.log` in; when present the facade emits one `warn` line per FAILED
|
|
317
|
+
* delivery — trace-correlated to the enclosing action and durably archived by
|
|
318
|
+
* the log sink. Successes and prunes stay off the log to keep the archive
|
|
319
|
+
* clean; they are counted on `metrics` instead. Absent ⇒ no log emits.
|
|
320
|
+
*/
|
|
321
|
+
log?: NotifyLogger;
|
|
322
|
+
/**
|
|
323
|
+
* The request's `ctx.metrics` (structural {@link NotifyMetrics}). Codegen
|
|
324
|
+
* threads `ctx.metrics` in; when present the facade counts every send on the
|
|
325
|
+
* `notify.send` series (dimensions `channel` / `provider` / `status`) and every
|
|
326
|
+
* no-op on `notify.skipped` (`channel` / `reason`) — feeding the durable metric
|
|
327
|
+
* history + trend charts. Absent ⇒ no metric emits.
|
|
328
|
+
*/
|
|
329
|
+
metrics?: NotifyMetrics;
|
|
271
330
|
/** Suppress the in-memory-store dev warning (tests set this). */
|
|
272
331
|
silent?: boolean;
|
|
273
332
|
}
|
|
@@ -422,4 +481,4 @@ declare const targetOf: (subscription: StoredSubscription) => string;
|
|
|
422
481
|
* (a cert/session expiry) can never permanently drop a valid subscription.
|
|
423
482
|
*/
|
|
424
483
|
declare const isGoneError: (message: string | undefined) => boolean;
|
|
425
|
-
export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions, type D1Like, type D1PreparedLike, type D1StoreOptions, FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyEnv, type PushBroadcastJob, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore, WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1SubscriptionStore, defineNotify, enqueuePushBroadcast, fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastJob, targetOf, webPushFromEnv, webPushId };
|
|
484
|
+
export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions, type D1Like, type D1PreparedLike, type D1StoreOptions, 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, WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1SubscriptionStore, defineNotify, enqueuePushBroadcast, fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastJob, targetOf, webPushFromEnv, webPushId };
|
package/dist/index.d.ts
CHANGED
|
@@ -119,6 +119,49 @@ interface BroadcastResult {
|
|
|
119
119
|
/** Total subscriptions attempted. */
|
|
120
120
|
total: number;
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The compact, stable delivery-status vocabulary emitted on notify observability
|
|
124
|
+
* signals — the `status` dimension on the `notify.send` metric and the failure
|
|
125
|
+
* log line. Modeled on Novu's execution status, but honest to edge push:
|
|
126
|
+
*
|
|
127
|
+
* - `accepted` — the provider took the message (a `Receipt.successful` send).
|
|
128
|
+
* - `failed` — a provider error; the log line carries the `error` text.
|
|
129
|
+
* - `gone` — the endpoint is unregistered (404/410, FCM `UNREGISTERED`) and pruned; push-only.
|
|
130
|
+
*
|
|
131
|
+
* Web Push and FCM give no delivery/open receipts, so the vocabulary stops at the
|
|
132
|
+
* send attempt: a `delivered`/`opened` status would be a lie for these channels.
|
|
133
|
+
* The one place a later `seen`/`read` is real is the in-app inbox, where the
|
|
134
|
+
* client posts a read receipt back — out of scope here.
|
|
135
|
+
*/
|
|
136
|
+
type NotifyDeliveryStatus = "accepted" | "failed" | "gone";
|
|
137
|
+
/**
|
|
138
|
+
* Why a send fanned out to nobody — the "sent 0 because…" signal (mirrors Novu's
|
|
139
|
+
* pre-send `DetailEnum` reasons). Emitted as the `reason` dimension on a
|
|
140
|
+
* `notify.skipped` metric so a no-op is visible in the Studio metric/trend view
|
|
141
|
+
* instead of silent.
|
|
142
|
+
*
|
|
143
|
+
* - `no-subscriptions-matched` — the store held no device for the broadcast filter.
|
|
144
|
+
* - `channel-not-configured` — the target channel was never wired in `defineNotify`.
|
|
145
|
+
*/
|
|
146
|
+
type NotifySkipReason = "channel-not-configured" | "no-subscriptions-matched";
|
|
147
|
+
/**
|
|
148
|
+
* The minimal structural slice of `ctx.log` the notify facade emits through — just
|
|
149
|
+
* the `warn` severity it uses for a failed delivery. Structural (rather than a
|
|
150
|
+
* dependency on `@lunora/server`'s `LunoraLogger`) so codegen passes the real
|
|
151
|
+
* `ctx.log` and a test passes a spy — the D1-store `D1Like` pattern, applied to
|
|
152
|
+
* observability.
|
|
153
|
+
*/
|
|
154
|
+
interface NotifyLogger {
|
|
155
|
+
warn: (message: string, fields?: Record<string, unknown>) => void;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The minimal structural slice of `ctx.metrics` the notify facade emits through —
|
|
159
|
+
* the `count` instrument backing the `notify.send` / `notify.skipped` series.
|
|
160
|
+
* Structural for the same reason as {@link NotifyLogger}.
|
|
161
|
+
*/
|
|
162
|
+
interface NotifyMetrics {
|
|
163
|
+
count: (name: string, value?: number, attributes?: Record<string, unknown>) => void;
|
|
164
|
+
}
|
|
122
165
|
/**
|
|
123
166
|
* The push sub-facade — spliced onto ctx as `ctx.push` (and reachable as
|
|
124
167
|
* `ctx.notify.push`). Owns the device-subscription lifecycle plus targeted and
|
|
@@ -268,6 +311,22 @@ interface CreateNotifyOptions {
|
|
|
268
311
|
* config resolution entirely.
|
|
269
312
|
*/
|
|
270
313
|
engine?: Notification;
|
|
314
|
+
/**
|
|
315
|
+
* The request's `ctx.log` (structural {@link NotifyLogger}). Codegen threads
|
|
316
|
+
* `ctx.log` in; when present the facade emits one `warn` line per FAILED
|
|
317
|
+
* delivery — trace-correlated to the enclosing action and durably archived by
|
|
318
|
+
* the log sink. Successes and prunes stay off the log to keep the archive
|
|
319
|
+
* clean; they are counted on `metrics` instead. Absent ⇒ no log emits.
|
|
320
|
+
*/
|
|
321
|
+
log?: NotifyLogger;
|
|
322
|
+
/**
|
|
323
|
+
* The request's `ctx.metrics` (structural {@link NotifyMetrics}). Codegen
|
|
324
|
+
* threads `ctx.metrics` in; when present the facade counts every send on the
|
|
325
|
+
* `notify.send` series (dimensions `channel` / `provider` / `status`) and every
|
|
326
|
+
* no-op on `notify.skipped` (`channel` / `reason`) — feeding the durable metric
|
|
327
|
+
* history + trend charts. Absent ⇒ no metric emits.
|
|
328
|
+
*/
|
|
329
|
+
metrics?: NotifyMetrics;
|
|
271
330
|
/** Suppress the in-memory-store dev warning (tests set this). */
|
|
272
331
|
silent?: boolean;
|
|
273
332
|
}
|
|
@@ -422,4 +481,4 @@ declare const targetOf: (subscription: StoredSubscription) => string;
|
|
|
422
481
|
* (a cert/session expiry) can never permanently drop a valid subscription.
|
|
423
482
|
*/
|
|
424
483
|
declare const isGoneError: (message: string | undefined) => boolean;
|
|
425
|
-
export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions, type D1Like, type D1PreparedLike, type D1StoreOptions, FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyEnv, type PushBroadcastJob, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore, WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1SubscriptionStore, defineNotify, enqueuePushBroadcast, fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastJob, targetOf, webPushFromEnv, webPushId };
|
|
484
|
+
export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions, type D1Like, type D1PreparedLike, type D1StoreOptions, 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, WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1SubscriptionStore, defineNotify, enqueuePushBroadcast, fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastJob, targetOf, webPushFromEnv, webPushId };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { FCM_ENV_KEYS, WEB_PUSH_ENV_KEYS, fcmFromEnv, webPushFromEnv } from './packem_shared/FCM_ENV_KEYS-DY4-A717.mjs';
|
|
2
2
|
export { defineNotify, isNotifyDefinition } from './packem_shared/defineNotify-B6S_47C2.mjs';
|
|
3
|
-
export { createNotify } from './packem_shared/createNotify-
|
|
3
|
+
export { createNotify } from './packem_shared/createNotify-bDWs4qBm.mjs';
|
|
4
4
|
export { buildEngine, routingPushProvider } from './packem_shared/buildEngine-DlmjvnNk.mjs';
|
|
5
5
|
export { enqueuePushBroadcast, runPushBroadcastJob } from './packem_shared/enqueuePushBroadcast-DiK7Hyja.mjs';
|
|
6
6
|
export { d1SubscriptionStore } from './packem_shared/d1SubscriptionStore-Dv3VPMI_.mjs';
|
|
@@ -5,6 +5,12 @@ import { normalizeRegisterInput, targetOf, isGoneError } from './fcmId-B-YPgHi7.
|
|
|
5
5
|
|
|
6
6
|
const resolveMaybeFactory = (value, env) => typeof value === "function" ? value(env) : value;
|
|
7
7
|
const receiptError = (receipt) => receipt.successful ? void 0 : receipt.errorMessages.join("; ");
|
|
8
|
+
const pushDeliveryStatus = (receipt, error) => {
|
|
9
|
+
if (receipt.successful) {
|
|
10
|
+
return "accepted";
|
|
11
|
+
}
|
|
12
|
+
return isGoneError(error) ? "gone" : "failed";
|
|
13
|
+
};
|
|
8
14
|
const mapWithConcurrency = async (items, limit, task) => {
|
|
9
15
|
const results = Array.from({ length: items.length });
|
|
10
16
|
let cursor = 0;
|
|
@@ -63,6 +69,16 @@ const createNotify = (definition, env, options = {}) => {
|
|
|
63
69
|
}
|
|
64
70
|
const subscriptionStore = store;
|
|
65
71
|
const concurrency = Math.max(1, options.concurrency ?? 10);
|
|
72
|
+
const { log, metrics } = options;
|
|
73
|
+
const countSend = (channel, provider, status, count = 1) => {
|
|
74
|
+
metrics?.count("notify.send", count, { channel, provider: provider ?? channel, status });
|
|
75
|
+
};
|
|
76
|
+
const warnFailedSend = (channel, provider, fields) => {
|
|
77
|
+
log?.warn(`notify ${channel} delivery failed`, { channel, provider: provider ?? channel, status: "failed", ...fields });
|
|
78
|
+
};
|
|
79
|
+
const observeSkip = (channel, reason) => {
|
|
80
|
+
metrics?.count("notify.skipped", 1, { channel, reason });
|
|
81
|
+
};
|
|
66
82
|
const resolveSubscription = async (target) => {
|
|
67
83
|
if (typeof target !== "string") {
|
|
68
84
|
return target;
|
|
@@ -73,28 +89,54 @@ const createNotify = (definition, env, options = {}) => {
|
|
|
73
89
|
}
|
|
74
90
|
return found;
|
|
75
91
|
};
|
|
76
|
-
const deliver = async (subscription, payload) => {
|
|
92
|
+
const deliver = async (subscription, payload, countInline) => {
|
|
77
93
|
const receipt = await engine.sendToChannel("push", { ...payload, to: targetOf(subscription) });
|
|
78
94
|
const error = receiptError(receipt);
|
|
79
|
-
|
|
95
|
+
const status = pushDeliveryStatus(receipt, error);
|
|
96
|
+
if (status === "accepted") {
|
|
80
97
|
await subscriptionStore.markStatus(subscription.id, "ok");
|
|
81
|
-
} else if (
|
|
98
|
+
} else if (status === "gone") {
|
|
82
99
|
await subscriptionStore.delete(subscription.id);
|
|
83
100
|
} else {
|
|
84
101
|
await subscriptionStore.markStatus(subscription.id, "failed", error);
|
|
85
102
|
}
|
|
86
|
-
|
|
103
|
+
if (status === "failed") {
|
|
104
|
+
warnFailedSend("push", subscription.kind, { error, subscriptionId: subscription.id, userId: subscription.userId ?? null });
|
|
105
|
+
}
|
|
106
|
+
if (countInline) {
|
|
107
|
+
countSend("push", subscription.kind, status);
|
|
108
|
+
}
|
|
109
|
+
return { receipt, status };
|
|
87
110
|
};
|
|
88
111
|
const push = {
|
|
89
112
|
broadcast: async (payload, filter) => {
|
|
90
113
|
const subscriptions = await subscriptionStore.list(filter);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
if (subscriptions.length === 0) {
|
|
115
|
+
observeSkip("push", "no-subscriptions-matched");
|
|
116
|
+
}
|
|
117
|
+
const rows = await mapWithConcurrency(subscriptions, concurrency, async (subscription) => {
|
|
118
|
+
const { receipt, status } = await deliver(subscription, payload, false);
|
|
119
|
+
return { kind: subscription.kind, receipt, status, subscription };
|
|
120
|
+
});
|
|
121
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const { kind, status } of rows) {
|
|
123
|
+
const key = `${kind}\0${status}`;
|
|
124
|
+
const bucket = buckets.get(key);
|
|
125
|
+
if (bucket === void 0) {
|
|
126
|
+
buckets.set(key, { count: 1, kind, status });
|
|
127
|
+
} else {
|
|
128
|
+
bucket.count += 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const { count, kind, status } of buckets.values()) {
|
|
132
|
+
countSend("push", kind, status, count);
|
|
133
|
+
}
|
|
134
|
+
const outcomes = rows.map(({ receipt, status, subscription }) => {
|
|
135
|
+
if (status === "accepted") {
|
|
94
136
|
return { id: subscription.id, status: "ok" };
|
|
95
137
|
}
|
|
96
138
|
const error = receiptError(receipt);
|
|
97
|
-
return
|
|
139
|
+
return status === "gone" ? { error, id: subscription.id, status: "expired" } : { error, id: subscription.id, status: "failed" };
|
|
98
140
|
});
|
|
99
141
|
return {
|
|
100
142
|
failed: outcomes.filter((outcome) => outcome.status === "failed").length,
|
|
@@ -106,20 +148,41 @@ const createNotify = (definition, env, options = {}) => {
|
|
|
106
148
|
},
|
|
107
149
|
list: (filter) => subscriptionStore.list(filter),
|
|
108
150
|
register: (input) => subscriptionStore.put(normalizeRegisterInput(input)),
|
|
109
|
-
send: async (target, payload) =>
|
|
151
|
+
send: async (target, payload) => {
|
|
152
|
+
const { receipt } = await deliver(await resolveSubscription(target), payload, true);
|
|
153
|
+
return receipt;
|
|
154
|
+
},
|
|
110
155
|
unregister: (id) => subscriptionStore.delete(id)
|
|
111
156
|
};
|
|
112
157
|
const sendToChannel = async (channel, payload) => {
|
|
113
158
|
if (engine.getProvider(channel) === void 0) {
|
|
159
|
+
observeSkip(channel, "channel-not-configured");
|
|
114
160
|
throw new LunoraError("BAD_REQUEST", `@lunora/notify: the "${channel}" channel is not configured in defineNotify(...)`);
|
|
115
161
|
}
|
|
116
|
-
|
|
162
|
+
const receipt = await engine.sendToChannel(channel, payload);
|
|
163
|
+
const status = receipt.successful ? "accepted" : "failed";
|
|
164
|
+
countSend(channel, receipt.provider, status);
|
|
165
|
+
if (status === "failed") {
|
|
166
|
+
warnFailedSend(channel, receipt.provider, { error: receiptError(receipt) });
|
|
167
|
+
}
|
|
168
|
+
return receipt;
|
|
117
169
|
};
|
|
118
170
|
const notify = {
|
|
119
171
|
chat: (payload) => sendToChannel("chat", payload),
|
|
120
172
|
inApp: (payload) => sendToChannel("inapp", payload),
|
|
121
173
|
push,
|
|
122
|
-
send: (message) =>
|
|
174
|
+
send: async (message) => {
|
|
175
|
+
const receipts = await engine.send(message);
|
|
176
|
+
for (const receipt of receipts) {
|
|
177
|
+
const channel = receipt.channel ?? "unknown";
|
|
178
|
+
const status = receipt.successful ? "accepted" : "failed";
|
|
179
|
+
countSend(channel, receipt.provider, status);
|
|
180
|
+
if (status === "failed") {
|
|
181
|
+
warnFailedSend(channel, receipt.provider, { error: receiptError(receipt) });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return receipts;
|
|
185
|
+
},
|
|
123
186
|
webhook: (payload) => sendToChannel("webhook", payload)
|
|
124
187
|
};
|
|
125
188
|
return { notify, push };
|
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.2",
|
|
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",
|