easy-ping 0.4.0 → 0.6.0

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.
Files changed (140) hide show
  1. package/README.md +548 -427
  2. package/dist/adapter-B7R4rXtJ.d.cts +17 -0
  3. package/dist/adapter-B7R4rXtJ.d.ts +17 -0
  4. package/dist/{adapter-BfLe2J0t.d.cts → adapter-D3lTy-4E.d.cts} +6 -32
  5. package/dist/{adapter-BfLe2J0t.d.ts → adapter-D3lTy-4E.d.ts} +6 -32
  6. package/dist/adapters/drizzle.cjs +1 -2
  7. package/dist/adapters/drizzle.cjs.map +1 -1
  8. package/dist/adapters/drizzle.d.cts +1 -1
  9. package/dist/adapters/drizzle.d.ts +1 -1
  10. package/dist/adapters/drizzle.js +3 -4
  11. package/dist/adapters/drizzle.js.map +1 -1
  12. package/dist/adapters/mongodb.cjs +144 -5
  13. package/dist/adapters/mongodb.cjs.map +1 -1
  14. package/dist/adapters/mongodb.d.cts +43 -18
  15. package/dist/adapters/mongodb.d.ts +43 -18
  16. package/dist/adapters/mongodb.js +77 -8
  17. package/dist/adapters/mongodb.js.map +1 -1
  18. package/dist/adapters/mysql.cjs +605 -0
  19. package/dist/adapters/mysql.cjs.map +1 -0
  20. package/dist/adapters/mysql.d.cts +38 -0
  21. package/dist/adapters/mysql.d.ts +38 -0
  22. package/dist/adapters/mysql.js +51 -0
  23. package/dist/adapters/mysql.js.map +1 -0
  24. package/dist/adapters/postgres.cjs +162 -9
  25. package/dist/adapters/postgres.cjs.map +1 -1
  26. package/dist/adapters/postgres.d.cts +44 -54
  27. package/dist/adapters/postgres.d.ts +44 -54
  28. package/dist/adapters/postgres.js +82 -42
  29. package/dist/adapters/postgres.js.map +1 -1
  30. package/dist/adapters/sqlite.cjs +593 -0
  31. package/dist/adapters/sqlite.cjs.map +1 -0
  32. package/dist/adapters/sqlite.d.cts +37 -0
  33. package/dist/adapters/sqlite.d.ts +37 -0
  34. package/dist/adapters/sqlite.js +53 -0
  35. package/dist/adapters/sqlite.js.map +1 -0
  36. package/dist/browser.cjs.map +1 -1
  37. package/dist/browser.d.cts +2 -14
  38. package/dist/browser.d.ts +2 -14
  39. package/dist/browser.js.map +1 -1
  40. package/dist/chunk-2EYKPF5A.js +393 -0
  41. package/dist/chunk-2EYKPF5A.js.map +1 -0
  42. package/dist/{chunk-3Y2MBUAW.js → chunk-3VOQOGDR.js} +4 -3
  43. package/dist/chunk-3VOQOGDR.js.map +1 -0
  44. package/dist/{chunk-PSJHXZPR.js → chunk-AEYYTE5I.js} +2 -2
  45. package/dist/chunk-AEYYTE5I.js.map +1 -0
  46. package/dist/chunk-CK5UPVYP.js +48 -0
  47. package/dist/chunk-CK5UPVYP.js.map +1 -0
  48. package/dist/{chunk-W6I3B2CI.js → chunk-E4IZ2CQC.js} +2 -2
  49. package/dist/{chunk-W6I3B2CI.js.map → chunk-E4IZ2CQC.js.map} +1 -1
  50. package/dist/chunk-HY47TQMY.js +52 -0
  51. package/dist/chunk-HY47TQMY.js.map +1 -0
  52. package/dist/{chunk-CYKAM7LE.js → chunk-JKTMUMQK.js} +31 -6
  53. package/dist/chunk-JKTMUMQK.js.map +1 -0
  54. package/dist/chunk-M4R77BJD.js +64 -0
  55. package/dist/chunk-M4R77BJD.js.map +1 -0
  56. package/dist/chunk-ORZKH224.js +75 -0
  57. package/dist/chunk-ORZKH224.js.map +1 -0
  58. package/dist/{chunk-DG3ZL5NP.js → chunk-USQ6CQTJ.js} +88 -3
  59. package/dist/chunk-USQ6CQTJ.js.map +1 -0
  60. package/dist/{chunk-YEMRRMJQ.js → chunk-XIDVLYNQ.js} +14 -4
  61. package/dist/chunk-XIDVLYNQ.js.map +1 -0
  62. package/dist/client.cjs +288 -17
  63. package/dist/client.cjs.map +1 -1
  64. package/dist/client.d.cts +53 -1
  65. package/dist/client.d.ts +53 -1
  66. package/dist/client.js +288 -17
  67. package/dist/client.js.map +1 -1
  68. package/dist/index.cjs +239 -9
  69. package/dist/index.cjs.map +1 -1
  70. package/dist/index.d.cts +53 -57
  71. package/dist/index.d.ts +53 -57
  72. package/dist/index.js +60 -13
  73. package/dist/index.js.map +1 -1
  74. package/dist/node.cjs +26 -5
  75. package/dist/node.cjs.map +1 -1
  76. package/dist/node.d.cts +1 -1
  77. package/dist/node.d.ts +1 -1
  78. package/dist/node.js +28 -6
  79. package/dist/node.js.map +1 -1
  80. package/dist/{plugin-C9Hoxus2.d.ts → plugin-CHd2ZiGC.d.cts} +5 -21
  81. package/dist/{plugin-CiMsGlQe.d.cts → plugin-Cg9usuUi.d.ts} +5 -21
  82. package/dist/plugins/digests.cjs +1 -4
  83. package/dist/plugins/digests.cjs.map +1 -1
  84. package/dist/plugins/digests.d.cts +6 -31
  85. package/dist/plugins/digests.d.ts +6 -31
  86. package/dist/plugins/digests.js +1 -4
  87. package/dist/plugins/digests.js.map +1 -1
  88. package/dist/plugins/preferences-client.cjs.map +1 -1
  89. package/dist/plugins/preferences-client.d.cts +2 -2
  90. package/dist/plugins/preferences-client.d.ts +2 -2
  91. package/dist/plugins/preferences-client.js +1 -1
  92. package/dist/plugins/preferences-react.cjs.map +1 -1
  93. package/dist/plugins/preferences-react.js.map +1 -1
  94. package/dist/plugins/preferences.cjs +1 -2
  95. package/dist/plugins/preferences.cjs.map +1 -1
  96. package/dist/plugins/preferences.d.cts +2 -2
  97. package/dist/plugins/preferences.d.ts +2 -2
  98. package/dist/plugins/preferences.js +3 -3
  99. package/dist/plugins/preferences.js.map +1 -1
  100. package/dist/plugins/push.cjs +1 -2
  101. package/dist/plugins/push.cjs.map +1 -1
  102. package/dist/plugins/push.d.cts +6 -26
  103. package/dist/plugins/push.d.ts +6 -26
  104. package/dist/plugins/push.js +4 -4
  105. package/dist/plugins/push.js.map +1 -1
  106. package/dist/providers/web-push.cjs.map +1 -1
  107. package/dist/providers/web-push.d.cts +3 -10
  108. package/dist/providers/web-push.d.ts +3 -10
  109. package/dist/providers/web-push.js.map +1 -1
  110. package/dist/react.cjs +311 -19
  111. package/dist/react.cjs.map +1 -1
  112. package/dist/react.d.cts +53 -4
  113. package/dist/react.d.ts +53 -4
  114. package/dist/react.js +311 -19
  115. package/dist/react.js.map +1 -1
  116. package/dist/schema.cjs +119 -1
  117. package/dist/schema.cjs.map +1 -1
  118. package/dist/schema.d.cts +20 -14
  119. package/dist/schema.d.ts +20 -14
  120. package/dist/schema.js +4 -2
  121. package/dist/signals-kPmOlKDX.d.cts +27 -0
  122. package/dist/signals-kPmOlKDX.d.ts +27 -0
  123. package/dist/sw.cjs +39 -0
  124. package/dist/sw.cjs.map +1 -0
  125. package/dist/sw.d.cts +52 -0
  126. package/dist/sw.d.ts +52 -0
  127. package/dist/sw.js +36 -0
  128. package/dist/sw.js.map +1 -0
  129. package/dist/testing.cjs +3 -19
  130. package/dist/testing.cjs.map +1 -1
  131. package/dist/testing.d.cts +4 -22
  132. package/dist/testing.d.ts +4 -22
  133. package/dist/testing.js +3 -19
  134. package/dist/testing.js.map +1 -1
  135. package/package.json +17 -1
  136. package/dist/chunk-3Y2MBUAW.js.map +0 -1
  137. package/dist/chunk-CYKAM7LE.js.map +0 -1
  138. package/dist/chunk-DG3ZL5NP.js.map +0 -1
  139. package/dist/chunk-PSJHXZPR.js.map +0 -1
  140. package/dist/chunk-YEMRRMJQ.js.map +0 -1
package/dist/index.d.cts CHANGED
@@ -1,10 +1,12 @@
1
- import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-BfLe2J0t.cjs';
2
- export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-BfLe2J0t.cjs';
1
+ import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-D3lTy-4E.cjs';
2
+ export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-D3lTy-4E.cjs';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
- import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-CiMsGlQe.cjs';
5
- export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-CiMsGlQe.cjs';
4
+ import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-CHd2ZiGC.cjs';
5
+ export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-CHd2ZiGC.cjs';
6
6
  import { E as EmailProvider } from './provider-CA45WVqt.cjs';
7
7
  export { a as EmailMessage, P as ProviderSendResult } from './provider-CA45WVqt.cjs';
8
+ import { S as Signals } from './signals-kPmOlKDX.cjs';
9
+ export { D as DELIVERIES_CHANNEL, c as createBridgedSignals, a as createMemorySignals, i as inboxChannel } from './signals-kPmOlKDX.cjs';
8
10
 
9
11
  declare function encodeBase64Url(input: string | Uint8Array): string;
10
12
  declare function decodeBase64Url(value: string): string | null;
@@ -23,10 +25,7 @@ type NotificationDefinition<TSchema extends StandardSchemaV1 | undefined = Stand
23
25
  };
24
26
  /** Binds TSchema so email callbacks infer the payload; TS cannot infer from a sibling property. */
25
27
  declare function defineNotification<TSchema extends StandardSchemaV1 | undefined = undefined>(definition: NotificationDefinition<TSchema>): NotificationDefinition<TSchema>;
26
- /**
27
- * Erased form. `EmailTemplate<never>` because callbacks are contravariant —
28
- * anything else makes every schema-typed definition fail to assign.
29
- */
28
+ /** Erased form. `EmailTemplate<never>` because callbacks are contravariant. */
30
29
  type AnyNotificationDefinition = {
31
30
  schema?: StandardSchemaV1 | undefined;
32
31
  channels: readonly Channel[];
@@ -42,19 +41,10 @@ type RateLimitConfig = {
42
41
  /** Requests allowed per key per window. */
43
42
  max: number;
44
43
  windowMs: number;
45
- /**
46
- * Which bucket a request lands in. Defaults to the client address as the
47
- * usual proxy headers report it; returning null exempts the request. Behind
48
- * no proxy those headers are absent, so supply your own key there.
49
- */
44
+ /** Bucket key. Defaults to the client address from proxy headers; null exempts the request. */
50
45
  key?: (request: Request) => string | null;
51
46
  };
52
- /**
53
- * Fixed-window counter held in process memory. Per isolate on edge runtimes
54
- * and per replica anywhere else, so it bounds one client's cost to one
55
- * process rather than enforcing a global quota. That is enough to blunt
56
- * secret guessing and poll storms, which is what it is for.
57
- */
47
+ /** Fixed window in process memory: per isolate or replica, so a bound rather than a quota. */
58
48
  declare function createRateLimiter(config: RateLimitConfig): (request: Request) => Response | null;
59
49
 
60
50
  type SessionConfig = {
@@ -70,11 +60,28 @@ type DeliveryConfig = {
70
60
  /** inline mode only. */
71
61
  throwOnError?: boolean;
72
62
  backoff?: "exponential" | ((attempt: number) => number);
63
+ /** Next.js `after` or Cloudflare `ctx.waitUntil`. Injected, not detected. */
64
+ waitUntil?: (promise: Promise<unknown>) => void;
73
65
  /**
74
- * Next.js `after`, Cloudflare `ctx.waitUntil`, or @vercel/functions.
75
- * Injected, not detected: importing a maybe-absent package breaks bundlers.
66
+ * Run one small claim-and-deliver pass after any request the handler serves,
67
+ * at most once per `everyMs` per process. Delivery then rides on your own
68
+ * traffic and the cron becomes a backstop. Ignored in `inline` mode. RFC 0006 idea 5.
76
69
  */
77
- waitUntil?: (promise: Promise<unknown>) => void;
70
+ sweepOnRequest?: true | {
71
+ everyMs?: number;
72
+ limit?: number;
73
+ };
74
+ };
75
+ type EventsConfig = {
76
+ /** Comment line keeping proxies from closing an idle stream. Default 25 s. */
77
+ heartbeatMs?: number;
78
+ /**
79
+ * Without a cross-process signal source, each stream checks the database
80
+ * for changes this often on its subscriber's behalf. Default 30 s; 0 disables.
81
+ */
82
+ probeIntervalMs?: number;
83
+ /** Close the stream after this long; the client reconnects. 0 (default) leaves it open. */
84
+ maxDurationMs?: number;
78
85
  };
79
86
  type ChannelsConfig = {
80
87
  inApp?: {
@@ -99,25 +106,22 @@ type EasyPingConfig<TDefs extends NotificationDefinitions> = {
99
106
  /** Sweeps one POST /cron may run before returning. Defaults to 50. */
100
107
  maxSweeps?: number;
101
108
  };
102
- /**
103
- * Bearer secret for plugins' machine routes (`/push/prune`, `/digests/cron`).
104
- * Falls back to `cron.secret`; set it when the scheduler that hits /cron
105
- * should not also be able to prune devices or fire digests.
106
- */
109
+ /** Bearer secret for plugin machine routes. Falls back to `cron.secret`. */
107
110
  machineSecret?: string;
111
+ /** In-process fixed-window limiter, applied before routing. */
112
+ rateLimit?: RateLimitConfig;
108
113
  /**
109
- * In-process fixed-window limiter, applied before routing. Keyed by client
110
- * address from the usual proxy headers unless `key` says otherwise.
114
+ * How "something changed" travels between the sender, the worker and open
115
+ * event streams. Defaults to in-process. Supply a LISTEN/NOTIFY or change
116
+ * stream implementation to cross replicas. RFC 0006 §4C.
111
117
  */
112
- rateLimit?: RateLimitConfig;
118
+ signals?: Signals;
119
+ /** `GET /events`, a Server-Sent Events stream per user. `false` unmounts it. */
120
+ events?: EventsConfig | false;
113
121
  plugins?: readonly AnyPlugin[];
114
122
  /** Where the handler is mounted, used to strip the prefix off incoming URLs. */
115
123
  basePath?: string;
116
- /**
117
- * Origins allowed to POST besides the request's own host. Cross-origin
118
- * POSTs are otherwise refused with 403, and every POST must be
119
- * `application/json`. `"*.example.com"` matches subdomains.
120
- */
124
+ /** Origins other than the request host that may POST. `"*.example.com"` matches subdomains. */
121
125
  trustedOrigins?: readonly string[];
122
126
  /** Runs before routing. Return a Response to short-circuit — a rate limiter's 429, say. */
123
127
  onRequest?: (request: Request) => Promisable<Response | undefined | void>;
@@ -167,6 +171,8 @@ type HealthReport = {
167
171
  cronRequiredButMissing: boolean;
168
172
  warnings: readonly string[];
169
173
  };
174
+ /** Response header carrying a per-user inbox version, so app traffic can replace polling. */
175
+ declare const INBOX_VERSION_HEADER = "x-easy-ping-inbox";
170
176
  type EasyPing<TDefs extends NotificationDefinitions> = {
171
177
  send<TKey extends keyof TDefs & string>(type: TKey, args: SendArgs<TDefs, TKey>): Promise<SendResult>;
172
178
  handler: {
@@ -175,6 +181,7 @@ type EasyPing<TDefs extends NotificationDefinitions> = {
175
181
  GET: (request: Request) => Promise<Response>;
176
182
  POST: (request: Request) => Promise<Response>;
177
183
  };
184
+ /** Idle interval defaults to 10 s; a send in this process wakes the loop at once. */
178
185
  startWorker(options?: {
179
186
  intervalMs?: number;
180
187
  batchSize?: number;
@@ -183,12 +190,12 @@ type EasyPing<TDefs extends NotificationDefinitions> = {
183
190
  /** Every mounted route with its auth scope, so `custom`-scoped ones stay visible. RFC 0002 §3. */
184
191
  listRoutes(): readonly MountedRoute[];
185
192
  /**
186
- * Deliveries that exhausted their attempts, newest first.
187
- *
188
- * The retry machinery is otherwise a black box: rows go quiet and the only
189
- * way to ask what happened is SQL against tables the library owns. Wire this
190
- * to an admin page or an alert.
193
+ * Headers to attach to any response your app sends this user. The client's
194
+ * `instrument(fetch)` reads them and refreshes only when the version moved,
195
+ * so an active user never needs a dedicated poll. RFC 0006 idea 1.
191
196
  */
197
+ inboxHeaders(userId: string): Record<string, string>;
198
+ /** Deliveries that exhausted their attempts, newest first. Wire it to an admin page or an alert. */
192
199
  getFailedDeliveries(options?: {
193
200
  /** Defaults to the last 24 hours. */
194
201
  since?: Date;
@@ -209,6 +216,8 @@ type RunnerDeps = {
209
216
  leaseMs: number;
210
217
  batchSize: number;
211
218
  backoff: Backoff;
219
+ /** Wakes an idle worker the moment a send commits, so the idle interval can be long. */
220
+ signals?: Signals | undefined;
212
221
  };
213
222
  type SweepResult = {
214
223
  claimed: number;
@@ -217,11 +226,7 @@ type SweepResult = {
217
226
  /** Deliveries that had nothing to do. Not errors; see DeliveryOutcome. */
218
227
  skipped: number;
219
228
  };
220
- /**
221
- * What gets persisted to `last_error`. Provider responses quote the offending
222
- * field, which for a bad recipient is the address itself; that column outlives
223
- * the notification and is read by admin tooling, so addresses do not go in.
224
- */
229
+ /** Persisted to `last_error`, which admin tooling reads: addresses do not go in. */
225
230
  declare const redactErrorMessage: (text: string) => string;
226
231
  declare function createRunner(deps: RunnerDeps): {
227
232
  runOnce: (options?: {
@@ -248,22 +253,13 @@ type JsonBody = {
248
253
  } | {
249
254
  error: Response;
250
255
  };
251
- /**
252
- * Reads a JSON object body with a byte cap enforced while streaming, so an
253
- * oversized body is refused before it is buffered. Malformed JSON is 400; an
254
- * empty body is `{}`. Plugin routes should use this rather than
255
- * `request.json()`, which has no limit.
256
- */
256
+ /** Reads a JSON object body with a streaming byte cap. Malformed JSON is 400; an empty body is `{}`. */
257
257
  declare function readJsonBody(request: Request, maxBytes?: number): Promise<JsonBody>;
258
258
 
259
- /**
260
- * For email templates built with template literals. Anything in a payload
261
- * that a user typed — a display name, a comment excerpt — goes through this
262
- * or the recipient gets that user's HTML delivered from your sending domain.
263
- */
259
+ /** For template literals: anything a user typed goes through this, or their HTML ships from your domain. */
264
260
  declare const escapeHtml: (value: unknown) => string;
265
261
 
266
262
  declare const MIN_SECRET_LENGTH = 16;
267
263
  declare function easyPing<TDefs extends NotificationDefinitions>(config: EasyPingConfig<TDefs>): EasyPing<TDefs>;
268
264
 
269
- export { type AnyNotificationDefinition, AnyPlugin, Channel, type ChannelsConfig, DatabaseAdapter, type DeliveryConfig, DeliveryMode, DeliveryRecord, type EasyPing, type EasyPingConfig, EmailProvider, type EmailTemplate, type HealthReport, type JsonBody, Logger, MAX_BODY_BYTES, MIN_SECRET_LENGTH, type MountedRoute, type NotificationDefinition, type NotificationDefinitions, type PayloadFromSchema, type PayloadOf, Promisable, type RateLimitConfig, Recipient, RouteScope, type Runner, type RunnerDeps, type SendArgs, type SendResult, type SessionConfig, SkipReason, type SweepResult, type Worker, createRateLimiter, createRunner, decodeBase64Url, defineNotification, easyPing, encodeBase64Url, escapeHtml, readJsonBody, redactErrorMessage };
265
+ export { type AnyNotificationDefinition, AnyPlugin, Channel, type ChannelsConfig, DatabaseAdapter, type DeliveryConfig, DeliveryMode, DeliveryRecord, type EasyPing, type EasyPingConfig, EmailProvider, type EmailTemplate, type EventsConfig, type HealthReport, INBOX_VERSION_HEADER, type JsonBody, Logger, MAX_BODY_BYTES, MIN_SECRET_LENGTH, type MountedRoute, type NotificationDefinition, type NotificationDefinitions, type PayloadFromSchema, type PayloadOf, Promisable, type RateLimitConfig, Recipient, RouteScope, type Runner, type RunnerDeps, type SendArgs, type SendResult, type SessionConfig, Signals, SkipReason, type SweepResult, type Worker, createRateLimiter, createRunner, decodeBase64Url, defineNotification, easyPing, encodeBase64Url, escapeHtml, readJsonBody, redactErrorMessage };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-BfLe2J0t.js';
2
- export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-BfLe2J0t.js';
1
+ import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-D3lTy-4E.js';
2
+ export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-D3lTy-4E.js';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
- import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-C9Hoxus2.js';
5
- export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-C9Hoxus2.js';
4
+ import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-Cg9usuUi.js';
5
+ export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-Cg9usuUi.js';
6
6
  import { E as EmailProvider } from './provider-CA45WVqt.js';
7
7
  export { a as EmailMessage, P as ProviderSendResult } from './provider-CA45WVqt.js';
8
+ import { S as Signals } from './signals-kPmOlKDX.js';
9
+ export { D as DELIVERIES_CHANNEL, c as createBridgedSignals, a as createMemorySignals, i as inboxChannel } from './signals-kPmOlKDX.js';
8
10
 
9
11
  declare function encodeBase64Url(input: string | Uint8Array): string;
10
12
  declare function decodeBase64Url(value: string): string | null;
@@ -23,10 +25,7 @@ type NotificationDefinition<TSchema extends StandardSchemaV1 | undefined = Stand
23
25
  };
24
26
  /** Binds TSchema so email callbacks infer the payload; TS cannot infer from a sibling property. */
25
27
  declare function defineNotification<TSchema extends StandardSchemaV1 | undefined = undefined>(definition: NotificationDefinition<TSchema>): NotificationDefinition<TSchema>;
26
- /**
27
- * Erased form. `EmailTemplate<never>` because callbacks are contravariant —
28
- * anything else makes every schema-typed definition fail to assign.
29
- */
28
+ /** Erased form. `EmailTemplate<never>` because callbacks are contravariant. */
30
29
  type AnyNotificationDefinition = {
31
30
  schema?: StandardSchemaV1 | undefined;
32
31
  channels: readonly Channel[];
@@ -42,19 +41,10 @@ type RateLimitConfig = {
42
41
  /** Requests allowed per key per window. */
43
42
  max: number;
44
43
  windowMs: number;
45
- /**
46
- * Which bucket a request lands in. Defaults to the client address as the
47
- * usual proxy headers report it; returning null exempts the request. Behind
48
- * no proxy those headers are absent, so supply your own key there.
49
- */
44
+ /** Bucket key. Defaults to the client address from proxy headers; null exempts the request. */
50
45
  key?: (request: Request) => string | null;
51
46
  };
52
- /**
53
- * Fixed-window counter held in process memory. Per isolate on edge runtimes
54
- * and per replica anywhere else, so it bounds one client's cost to one
55
- * process rather than enforcing a global quota. That is enough to blunt
56
- * secret guessing and poll storms, which is what it is for.
57
- */
47
+ /** Fixed window in process memory: per isolate or replica, so a bound rather than a quota. */
58
48
  declare function createRateLimiter(config: RateLimitConfig): (request: Request) => Response | null;
59
49
 
60
50
  type SessionConfig = {
@@ -70,11 +60,28 @@ type DeliveryConfig = {
70
60
  /** inline mode only. */
71
61
  throwOnError?: boolean;
72
62
  backoff?: "exponential" | ((attempt: number) => number);
63
+ /** Next.js `after` or Cloudflare `ctx.waitUntil`. Injected, not detected. */
64
+ waitUntil?: (promise: Promise<unknown>) => void;
73
65
  /**
74
- * Next.js `after`, Cloudflare `ctx.waitUntil`, or @vercel/functions.
75
- * Injected, not detected: importing a maybe-absent package breaks bundlers.
66
+ * Run one small claim-and-deliver pass after any request the handler serves,
67
+ * at most once per `everyMs` per process. Delivery then rides on your own
68
+ * traffic and the cron becomes a backstop. Ignored in `inline` mode. RFC 0006 idea 5.
76
69
  */
77
- waitUntil?: (promise: Promise<unknown>) => void;
70
+ sweepOnRequest?: true | {
71
+ everyMs?: number;
72
+ limit?: number;
73
+ };
74
+ };
75
+ type EventsConfig = {
76
+ /** Comment line keeping proxies from closing an idle stream. Default 25 s. */
77
+ heartbeatMs?: number;
78
+ /**
79
+ * Without a cross-process signal source, each stream checks the database
80
+ * for changes this often on its subscriber's behalf. Default 30 s; 0 disables.
81
+ */
82
+ probeIntervalMs?: number;
83
+ /** Close the stream after this long; the client reconnects. 0 (default) leaves it open. */
84
+ maxDurationMs?: number;
78
85
  };
79
86
  type ChannelsConfig = {
80
87
  inApp?: {
@@ -99,25 +106,22 @@ type EasyPingConfig<TDefs extends NotificationDefinitions> = {
99
106
  /** Sweeps one POST /cron may run before returning. Defaults to 50. */
100
107
  maxSweeps?: number;
101
108
  };
102
- /**
103
- * Bearer secret for plugins' machine routes (`/push/prune`, `/digests/cron`).
104
- * Falls back to `cron.secret`; set it when the scheduler that hits /cron
105
- * should not also be able to prune devices or fire digests.
106
- */
109
+ /** Bearer secret for plugin machine routes. Falls back to `cron.secret`. */
107
110
  machineSecret?: string;
111
+ /** In-process fixed-window limiter, applied before routing. */
112
+ rateLimit?: RateLimitConfig;
108
113
  /**
109
- * In-process fixed-window limiter, applied before routing. Keyed by client
110
- * address from the usual proxy headers unless `key` says otherwise.
114
+ * How "something changed" travels between the sender, the worker and open
115
+ * event streams. Defaults to in-process. Supply a LISTEN/NOTIFY or change
116
+ * stream implementation to cross replicas. RFC 0006 §4C.
111
117
  */
112
- rateLimit?: RateLimitConfig;
118
+ signals?: Signals;
119
+ /** `GET /events`, a Server-Sent Events stream per user. `false` unmounts it. */
120
+ events?: EventsConfig | false;
113
121
  plugins?: readonly AnyPlugin[];
114
122
  /** Where the handler is mounted, used to strip the prefix off incoming URLs. */
115
123
  basePath?: string;
116
- /**
117
- * Origins allowed to POST besides the request's own host. Cross-origin
118
- * POSTs are otherwise refused with 403, and every POST must be
119
- * `application/json`. `"*.example.com"` matches subdomains.
120
- */
124
+ /** Origins other than the request host that may POST. `"*.example.com"` matches subdomains. */
121
125
  trustedOrigins?: readonly string[];
122
126
  /** Runs before routing. Return a Response to short-circuit — a rate limiter's 429, say. */
123
127
  onRequest?: (request: Request) => Promisable<Response | undefined | void>;
@@ -167,6 +171,8 @@ type HealthReport = {
167
171
  cronRequiredButMissing: boolean;
168
172
  warnings: readonly string[];
169
173
  };
174
+ /** Response header carrying a per-user inbox version, so app traffic can replace polling. */
175
+ declare const INBOX_VERSION_HEADER = "x-easy-ping-inbox";
170
176
  type EasyPing<TDefs extends NotificationDefinitions> = {
171
177
  send<TKey extends keyof TDefs & string>(type: TKey, args: SendArgs<TDefs, TKey>): Promise<SendResult>;
172
178
  handler: {
@@ -175,6 +181,7 @@ type EasyPing<TDefs extends NotificationDefinitions> = {
175
181
  GET: (request: Request) => Promise<Response>;
176
182
  POST: (request: Request) => Promise<Response>;
177
183
  };
184
+ /** Idle interval defaults to 10 s; a send in this process wakes the loop at once. */
178
185
  startWorker(options?: {
179
186
  intervalMs?: number;
180
187
  batchSize?: number;
@@ -183,12 +190,12 @@ type EasyPing<TDefs extends NotificationDefinitions> = {
183
190
  /** Every mounted route with its auth scope, so `custom`-scoped ones stay visible. RFC 0002 §3. */
184
191
  listRoutes(): readonly MountedRoute[];
185
192
  /**
186
- * Deliveries that exhausted their attempts, newest first.
187
- *
188
- * The retry machinery is otherwise a black box: rows go quiet and the only
189
- * way to ask what happened is SQL against tables the library owns. Wire this
190
- * to an admin page or an alert.
193
+ * Headers to attach to any response your app sends this user. The client's
194
+ * `instrument(fetch)` reads them and refreshes only when the version moved,
195
+ * so an active user never needs a dedicated poll. RFC 0006 idea 1.
191
196
  */
197
+ inboxHeaders(userId: string): Record<string, string>;
198
+ /** Deliveries that exhausted their attempts, newest first. Wire it to an admin page or an alert. */
192
199
  getFailedDeliveries(options?: {
193
200
  /** Defaults to the last 24 hours. */
194
201
  since?: Date;
@@ -209,6 +216,8 @@ type RunnerDeps = {
209
216
  leaseMs: number;
210
217
  batchSize: number;
211
218
  backoff: Backoff;
219
+ /** Wakes an idle worker the moment a send commits, so the idle interval can be long. */
220
+ signals?: Signals | undefined;
212
221
  };
213
222
  type SweepResult = {
214
223
  claimed: number;
@@ -217,11 +226,7 @@ type SweepResult = {
217
226
  /** Deliveries that had nothing to do. Not errors; see DeliveryOutcome. */
218
227
  skipped: number;
219
228
  };
220
- /**
221
- * What gets persisted to `last_error`. Provider responses quote the offending
222
- * field, which for a bad recipient is the address itself; that column outlives
223
- * the notification and is read by admin tooling, so addresses do not go in.
224
- */
229
+ /** Persisted to `last_error`, which admin tooling reads: addresses do not go in. */
225
230
  declare const redactErrorMessage: (text: string) => string;
226
231
  declare function createRunner(deps: RunnerDeps): {
227
232
  runOnce: (options?: {
@@ -248,22 +253,13 @@ type JsonBody = {
248
253
  } | {
249
254
  error: Response;
250
255
  };
251
- /**
252
- * Reads a JSON object body with a byte cap enforced while streaming, so an
253
- * oversized body is refused before it is buffered. Malformed JSON is 400; an
254
- * empty body is `{}`. Plugin routes should use this rather than
255
- * `request.json()`, which has no limit.
256
- */
256
+ /** Reads a JSON object body with a streaming byte cap. Malformed JSON is 400; an empty body is `{}`. */
257
257
  declare function readJsonBody(request: Request, maxBytes?: number): Promise<JsonBody>;
258
258
 
259
- /**
260
- * For email templates built with template literals. Anything in a payload
261
- * that a user typed — a display name, a comment excerpt — goes through this
262
- * or the recipient gets that user's HTML delivered from your sending domain.
263
- */
259
+ /** For template literals: anything a user typed goes through this, or their HTML ships from your domain. */
264
260
  declare const escapeHtml: (value: unknown) => string;
265
261
 
266
262
  declare const MIN_SECRET_LENGTH = 16;
267
263
  declare function easyPing<TDefs extends NotificationDefinitions>(config: EasyPingConfig<TDefs>): EasyPing<TDefs>;
268
264
 
269
- export { type AnyNotificationDefinition, AnyPlugin, Channel, type ChannelsConfig, DatabaseAdapter, type DeliveryConfig, DeliveryMode, DeliveryRecord, type EasyPing, type EasyPingConfig, EmailProvider, type EmailTemplate, type HealthReport, type JsonBody, Logger, MAX_BODY_BYTES, MIN_SECRET_LENGTH, type MountedRoute, type NotificationDefinition, type NotificationDefinitions, type PayloadFromSchema, type PayloadOf, Promisable, type RateLimitConfig, Recipient, RouteScope, type Runner, type RunnerDeps, type SendArgs, type SendResult, type SessionConfig, SkipReason, type SweepResult, type Worker, createRateLimiter, createRunner, decodeBase64Url, defineNotification, easyPing, encodeBase64Url, escapeHtml, readJsonBody, redactErrorMessage };
265
+ export { type AnyNotificationDefinition, AnyPlugin, Channel, type ChannelsConfig, DatabaseAdapter, type DeliveryConfig, DeliveryMode, DeliveryRecord, type EasyPing, type EasyPingConfig, EmailProvider, type EmailTemplate, type EventsConfig, type HealthReport, INBOX_VERSION_HEADER, type JsonBody, Logger, MAX_BODY_BYTES, MIN_SECRET_LENGTH, type MountedRoute, type NotificationDefinition, type NotificationDefinitions, type PayloadFromSchema, type PayloadOf, Promisable, type RateLimitConfig, Recipient, RouteScope, type Runner, type RunnerDeps, type SendArgs, type SendResult, type SessionConfig, Signals, SkipReason, type SweepResult, type Worker, createRateLimiter, createRunner, decodeBase64Url, defineNotification, easyPing, encodeBase64Url, escapeHtml, readJsonBody, redactErrorMessage };
package/dist/index.js CHANGED
@@ -1,12 +1,17 @@
1
- import { createRunner } from './chunk-CYKAM7LE.js';
2
- export { createRunner, redactErrorMessage } from './chunk-CYKAM7LE.js';
3
- export { defineClientPlugin, definePlugin } from './chunk-W6I3B2CI.js';
4
- import { createHandler, createScopedSigner } from './chunk-DG3ZL5NP.js';
5
- export { DEFAULT_TOKEN_TTL_SECONDS, MAX_BODY_BYTES, MAX_TOKEN_TTL_SECONDS, expiresIn, readJsonBody, signToken, verifyToken } from './chunk-DG3ZL5NP.js';
6
- import { consoleLogger, ConfigError, createPluginStore, ValidationError } from './chunk-3Y2MBUAW.js';
7
- export { ConfigError, EasyPingError, ValidationError, consoleLogger } from './chunk-3Y2MBUAW.js';
1
+ import { createRunner } from './chunk-JKTMUMQK.js';
2
+ export { createRunner, redactErrorMessage } from './chunk-JKTMUMQK.js';
3
+ export { defineClientPlugin, definePlugin } from './chunk-E4IZ2CQC.js';
4
+ import { createHandler, createScopedSigner } from './chunk-USQ6CQTJ.js';
5
+ export { DEFAULT_TOKEN_TTL_SECONDS, MAX_BODY_BYTES, MAX_TOKEN_TTL_SECONDS, expiresIn, readJsonBody, signToken, verifyToken } from './chunk-USQ6CQTJ.js';
6
+ import { createMemorySignals, inboxChannel, DELIVERIES_CHANNEL } from './chunk-ORZKH224.js';
7
+ export { DELIVERIES_CHANNEL, createBridgedSignals, createMemorySignals, inboxChannel } from './chunk-ORZKH224.js';
8
+ import { consoleLogger, ConfigError, createPluginStore, ValidationError } from './chunk-3VOQOGDR.js';
9
+ export { ConfigError, EasyPingError, ValidationError, consoleLogger } from './chunk-3VOQOGDR.js';
10
+ import './chunk-AEYYTE5I.js';
8
11
  export { decodeBase64Url, encodeBase64Url } from './chunk-PTGHMCCG.js';
9
- import './chunk-PSJHXZPR.js';
12
+
13
+ // src/core/config.ts
14
+ var INBOX_VERSION_HEADER = "x-easy-ping-inbox";
10
15
 
11
16
  // src/core/definition.ts
12
17
  function defineNotification(definition) {
@@ -197,8 +202,7 @@ async function runSend(type, input, deps) {
197
202
  id: newId(),
198
203
  channel: decision.channel,
199
204
  maxAttempts: definition.maxAttempts ?? deps.maxAttempts,
200
- // `defer` needs no machinery beyond not_before — the claim query
201
- // already refuses rows whose time has not come. RFC 0004 §4.2.
205
+ // defer is just not_before; the claim query refuses rows whose time has not come. RFC 0004 §4.2.
202
206
  notBefore: decision.action === "defer" && decision.notBefore ? decision.notBefore : now
203
207
  }));
204
208
  if (deliveries.length === 0) {
@@ -254,6 +258,7 @@ var CORE_ROUTES = [
254
258
  { method: "POST", path: "/seen", scope: { type: "user" }, owner: "core" },
255
259
  { method: "POST", path: "/read", scope: { type: "user" }, owner: "core" },
256
260
  { method: "POST", path: "/read-all", scope: { type: "user" }, owner: "core" },
261
+ { method: "GET", path: "/events", scope: { type: "user" }, owner: "core" },
257
262
  { method: "POST", path: "/cron", scope: { type: "machine" }, owner: "core" }
258
263
  ];
259
264
  var DEFAULTS = {
@@ -378,6 +383,20 @@ function easyPing(config) {
378
383
  }
379
384
  }
380
385
  for (const warning of warnings) logger.warn(warning);
386
+ const baseSignals = config.signals ?? createMemorySignals();
387
+ const inboxVersions = /* @__PURE__ */ new Map();
388
+ const signals = {
389
+ crossProcess: baseSignals.crossProcess,
390
+ publish(channel) {
391
+ if (channel.startsWith("inbox:")) {
392
+ if (inboxVersions.size > 5e4) inboxVersions.clear();
393
+ const userId = channel.slice("inbox:".length);
394
+ inboxVersions.set(userId, (inboxVersions.get(userId) ?? 0) + 1);
395
+ }
396
+ baseSignals.publish(channel);
397
+ },
398
+ subscribe: (channel, handler2) => baseSignals.subscribe(channel, handler2)
399
+ };
381
400
  const runner = createRunner({
382
401
  adapter: config.database,
383
402
  definitions: config.notifications,
@@ -387,15 +406,34 @@ function easyPing(config) {
387
406
  logger,
388
407
  leaseMs,
389
408
  batchSize: config.delivery?.batchSize ?? DEFAULTS.batchSize,
390
- backoff: config.delivery?.backoff ?? "exponential"
409
+ backoff: config.delivery?.backoff ?? "exponential",
410
+ signals
391
411
  });
392
412
  const pluginRoutes = plugins.flatMap((plugin) => plugin.routes ?? []);
413
+ const events = config.events === false ? false : {
414
+ heartbeatMs: config.events?.heartbeatMs ?? 25e3,
415
+ probeIntervalMs: config.events?.probeIntervalMs ?? 3e4,
416
+ maxDurationMs: config.events?.maxDurationMs ?? 0
417
+ };
418
+ const sweepConfig = config.delivery?.sweepOnRequest;
419
+ let lastRequestSweep = 0;
420
+ const sweepOnRequest = sweepConfig && mode !== "inline" ? () => {
421
+ const settings = sweepConfig === true ? {} : sweepConfig;
422
+ const now = Date.now();
423
+ if (now - lastRequestSweep < (settings.everyMs ?? 5e3)) return;
424
+ lastRequestSweep = now;
425
+ const pass = runner.runOnce({ limit: settings.limit ?? 5 }).catch((error) => logger.error("request-driven sweep failed", { error }));
426
+ config.delivery?.waitUntil?.(pass);
427
+ } : void 0;
393
428
  const handler = createHandler({
394
429
  adapter: config.database,
395
430
  session: config.session,
396
431
  runner,
397
432
  logger,
398
433
  secret: config.secret,
434
+ signals,
435
+ events,
436
+ sweepOnRequest,
399
437
  basePath: config.basePath ?? DEFAULTS.basePath,
400
438
  pluginRoutes,
401
439
  trustedOrigins: config.trustedOrigins,
@@ -427,6 +465,10 @@ function easyPing(config) {
427
465
  }
428
466
  );
429
467
  if (result.notifications.length > 0) {
468
+ for (const notification of result.notifications) {
469
+ signals.publish(inboxChannel(notification.userId));
470
+ }
471
+ signals.publish(DELIVERIES_CHANNEL);
430
472
  const ids = result.notifications.flatMap(
431
473
  (notification) => notification.deliveries.map((delivery) => delivery.id)
432
474
  );
@@ -459,8 +501,13 @@ function easyPing(config) {
459
501
  send,
460
502
  handler,
461
503
  startWorker: (options) => runner.startWorker(options ?? {}),
504
+ inboxHeaders: (userId) => ({
505
+ [INBOX_VERSION_HEADER]: String(inboxVersions.get(userId) ?? 0)
506
+ }),
462
507
  listRoutes: () => [
463
- ...CORE_ROUTES.filter((route) => route.path !== "/cron" || Boolean(config.cron?.secret)),
508
+ ...CORE_ROUTES.filter(
509
+ (route) => (route.path !== "/cron" || Boolean(config.cron?.secret)) && (route.path !== "/events" || events !== false)
510
+ ),
464
511
  ...plugins.flatMap(
465
512
  (plugin) => (plugin.routes ?? []).map((route) => ({
466
513
  method: route.method,
@@ -486,6 +533,6 @@ function easyPing(config) {
486
533
  };
487
534
  }
488
535
 
489
- export { MIN_SECRET_LENGTH, createRateLimiter, defineNotification, easyPing, escapeHtml };
536
+ export { INBOX_VERSION_HEADER, MIN_SECRET_LENGTH, createRateLimiter, defineNotification, easyPing, escapeHtml };
490
537
  //# sourceMappingURL=index.js.map
491
538
  //# sourceMappingURL=index.js.map