svelte-adapter-uws-extensions 0.4.2 → 0.5.0-next.10

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 (76) hide show
  1. package/MIGRATION.md +283 -0
  2. package/README.md +2987 -1045
  3. package/package.json +76 -6
  4. package/postgres/_tasks-errors.js +76 -0
  5. package/postgres/_tasks-sql.js +322 -0
  6. package/postgres/_tasks-worker-pool.js +183 -0
  7. package/postgres/_worker-harness.js +96 -0
  8. package/postgres/idempotency.d.ts +48 -0
  9. package/postgres/idempotency.js +279 -0
  10. package/postgres/index.d.ts +26 -6
  11. package/postgres/index.js +71 -24
  12. package/postgres/jobs.d.ts +99 -0
  13. package/postgres/jobs.js +285 -0
  14. package/postgres/notify.d.ts +48 -0
  15. package/postgres/notify.js +368 -242
  16. package/postgres/replay.d.ts +79 -1
  17. package/postgres/replay.js +121 -66
  18. package/postgres/tasks.d.ts +329 -0
  19. package/postgres/tasks.js +843 -0
  20. package/prometheus/index.d.ts +81 -0
  21. package/prometheus/index.js +203 -1
  22. package/redis/cursor.d.ts +20 -6
  23. package/redis/cursor.js +686 -695
  24. package/redis/fence.d.ts +31 -0
  25. package/redis/fence.js +125 -0
  26. package/redis/functions.d.ts +50 -0
  27. package/redis/functions.js +136 -0
  28. package/redis/groups.js +46 -37
  29. package/redis/idempotency.d.ts +65 -0
  30. package/redis/idempotency.js +194 -0
  31. package/redis/index.js +14 -1
  32. package/redis/leader.d.ts +72 -0
  33. package/redis/leader.js +228 -0
  34. package/redis/lock.d.ts +123 -0
  35. package/redis/lock.js +250 -0
  36. package/redis/presence.d.ts +49 -0
  37. package/redis/presence.js +541 -392
  38. package/redis/publish-rate.d.ts +116 -0
  39. package/redis/publish-rate.js +344 -0
  40. package/redis/pubsub.d.ts +48 -2
  41. package/redis/pubsub.js +176 -17
  42. package/redis/ratelimit.js +17 -58
  43. package/redis/registry.d.ts +220 -0
  44. package/redis/registry.js +1091 -0
  45. package/redis/replay-stream.js +390 -0
  46. package/redis/replay.d.ts +175 -0
  47. package/redis/replay.js +129 -66
  48. package/redis/session.d.ts +83 -0
  49. package/redis/session.js +202 -0
  50. package/redis/sharded-pubsub.d.ts +156 -0
  51. package/redis/sharded-pubsub.js +623 -0
  52. package/shared/admission.d.ts +87 -0
  53. package/shared/admission.js +169 -0
  54. package/shared/assert.d.ts +41 -0
  55. package/shared/assert.js +138 -0
  56. package/shared/breaker.d.ts +8 -0
  57. package/shared/breaker.js +182 -160
  58. package/shared/bus-validate.d.ts +79 -0
  59. package/shared/bus-validate.js +119 -0
  60. package/shared/caps.d.ts +27 -0
  61. package/shared/caps.js +110 -0
  62. package/shared/lease-scripts.js +50 -0
  63. package/shared/pg-migrate.js +40 -0
  64. package/shared/redis-scan.js +21 -0
  65. package/shared/redis-version.js +15 -0
  66. package/shared/replay-gate.js +25 -0
  67. package/shared/replay-helpers.js +126 -0
  68. package/shared/scripts.js +22 -6
  69. package/shared/sensitive.d.ts +58 -0
  70. package/shared/sensitive.js +100 -0
  71. package/shared/time.js +1 -1
  72. package/testing/index.d.ts +54 -5
  73. package/testing/index.js +23 -2
  74. package/testing/mock-pg.js +611 -11
  75. package/testing/mock-platform.js +104 -0
  76. package/testing/mock-redis.js +432 -12
package/MIGRATION.md ADDED
@@ -0,0 +1,283 @@
1
+ # Migration guide: svelte-adapter-uws-extensions 0.4.x to 0.5.x
2
+
3
+ This guide enumerates every breaking change between the latest 0.4.x release and the current 0.5.x line. Apply each step in order; sections are independent unless explicitly chained.
4
+
5
+ `svelte-adapter-uws-extensions` 0.5 raises its peerDep on `svelte-adapter-uws` to `^0.5.0-next.19` (or whatever the current next version is). See the adapter's MIGRATION.md for breaking changes on that side.
6
+
7
+ ## Breaking changes
8
+
9
+ ### Runtime: Node.js 22+ required (was Node 20+)
10
+
11
+ **What changed.** `package.json#engines.node` moved from `>=20.0.0` to `>=22.0.0`. Tracks the adapter's bump, which in turn tracks `uWebSockets.js` v20.67.0 dropping Node 20 support upstream.
12
+
13
+ **How to migrate.** See `svelte-adapter-uws/MIGRATION.md` for the full uWS-side rationale. No extensions-specific action beyond bumping your runtime to Node 22+.
14
+
15
+ ### 1. Postgres table prefix changed from `ws_` to `svti_`
16
+
17
+ **What changed.** All Postgres factories (`createReplay`, `createIdempotencyStore`, `createJobQueue`, `createTaskRunner`) now default to `svti_*` table names: `svti_replay`, `svti_replay_seq`, `svti_idempotency`, `svti_jobs`, `svti_tasks`. Primary key columns follow the `[tablename]_id` rule (`svti_replay_id`, `svti_jobs_id`, `svti_tasks_id`, `svti_idempotency_key`). The replay timestamp column was renamed `created_date` to `created_at`. JS-level field names on row objects are preserved via SQL `AS` aliases (`job.id`, `task.id`, `task.idempotency_key` are unchanged).
18
+
19
+ **How to migrate.** Pick one of:
20
+
21
+ Option A (zero-downtime, no SQL): override the `table` option on each module to keep the old names.
22
+
23
+ ```js
24
+ createReplay(pg, { table: 'ws_replay' });
25
+ createIdempotencyStore(pg, { table: 'ws_idempotency' });
26
+ createJobQueue(pg, { table: 'ws_jobs' });
27
+ createTaskRunner(pg, { table: 'ws_tasks' });
28
+ ```
29
+
30
+ Option B (rename in place):
31
+
32
+ ```sql
33
+ ALTER TABLE ws_replay RENAME TO svti_replay;
34
+ ALTER TABLE ws_replay RENAME COLUMN id TO svti_replay_id;
35
+ ALTER TABLE ws_replay RENAME COLUMN created_date TO created_at;
36
+ ALTER TABLE ws_replay_seq RENAME TO svti_replay_seq;
37
+ ALTER TABLE ws_idempotency RENAME TO svti_idempotency;
38
+ ALTER TABLE svti_idempotency RENAME COLUMN key TO svti_idempotency_key;
39
+ ALTER TABLE ws_jobs RENAME TO svti_jobs;
40
+ ALTER TABLE svti_jobs RENAME COLUMN id TO svti_jobs_id;
41
+ ALTER TABLE ws_tasks RENAME TO svti_tasks;
42
+ ALTER TABLE svti_tasks RENAME COLUMN task_id TO svti_tasks_id;
43
+ ALTER TABLE svti_tasks RENAME COLUMN idempotency_key TO svti_idempotency_key;
44
+ ```
45
+
46
+ ### 2. Postgres factories reject reserved-namespace table names
47
+
48
+ **What changed.** The `idempotency`, `jobs`, `replay`, and `tasks` factories now reject `table` values whose lowercase form starts with `pg_` or `information_schema` with an explicit "reserved Postgres schema" error. Pre-fix, these names passed the identifier-shape regex and could clash with Postgres internals.
49
+
50
+ **How to migrate.** Rename any custom `table` value that matches `pg_*` or `information_schema*` (extremely uncommon in practice). Standard prefixes are unaffected.
51
+
52
+ ### 3. Inbound bus envelopes are validated before republish
53
+
54
+ **What changed.** Every bus subscriber (`createPubSubBus`, `createShardedBus`, `createNotifyBridge`, `createCursor`) now gates inbound envelopes on raw byte size, topic shape, event shape, and an optional `__`-prefix denylist before republishing. Default `maxEnvelopeBytes` is 1 MB; default `allowSystemTopics` is `true` for backward compatibility, with the bus's own configured `systemChannel` (default `__realtime`) always allowlisted.
55
+
56
+ **How to migrate.** No code change required for default deployments. If your application publishes envelopes larger than 1 MB on the bus, raise `maxEnvelopeBytes` explicitly:
57
+
58
+ ```js
59
+ createPubSubBus(redis, { maxEnvelopeBytes: 4 * 1024 * 1024 });
60
+ createShardedBus(redis, { maxEnvelopeBytes: 4 * 1024 * 1024 });
61
+ createNotifyBridge(pg, { maxEnvelopeBytes: 4 * 1024 * 1024 });
62
+ createCursor(redis, { maxEnvelopeBytes: 4 * 1024 * 1024 });
63
+ ```
64
+
65
+ For defense in depth, opt out of system-prefixed user topics:
66
+
67
+ ```js
68
+ createPubSubBus(redis, { allowSystemTopics: false });
69
+ ```
70
+
71
+ Topics longer than 256 chars or containing control characters are now rejected; review any code that constructs topic names dynamically.
72
+
73
+ ### 4. Replay backends consult `platform.checkSubscribe` before reading history
74
+
75
+ **What changed.** Every `replay()` implementation (Redis sorted-set, Redis streams, Postgres) now calls `platform.checkSubscribe(ws, topic)` first. On denial, it sends a single `{event:'denied', data:{code:<denial>, reqId}}` frame on `__replay:{topic}` and returns without reading the buffer. Pre-fix, an attacker could send a crafted `lastSeenSeqs` map and read history for a topic they could not subscribe to live.
76
+
77
+ **How to migrate.** Client-side replay stores must handle the new `denied` event in addition to existing `truncated` / `end` events. Treat it similarly to `truncated`:
78
+
79
+ ```js
80
+ function onReplayFrame(event, data) {
81
+ if (event === 'denied') {
82
+ return;
83
+ }
84
+ }
85
+ ```
86
+
87
+ Older adapters lacking `checkSubscribe` degrade gracefully to the previous behavior.
88
+
89
+ ### 5. Tasks `idempotencyKey` is now namespaced by task name, with a 256-char cap
90
+
91
+ **What changed.** The cache key passed to `idempotency.acquire(...)` for `tasks.run` and `tasks.enqueue` is now `'task:' + name + ':' + idempotencyKey` (was the bare `idempotencyKey`). Pre-fix, two tasks sharing one client-supplied key shared one cache slot, which let a privileged task's cached result leak to a public task. Both methods also reject `idempotencyKey` longer than 256 characters; the underlying stores cap `acquire(key)` at 1024 chars as defense in depth.
92
+
93
+ **How to migrate.** No source change at call sites. In-flight cached entries from before the upgrade become invisible after deploy because the namespaced key does not match the old un-namespaced key. For Redis stores the old keys TTL out; for Postgres the cleanup interval handles them. Audit any code that supplies long `idempotencyKey` strings (request bodies, opaque tokens) and shorten them if they exceed 256 chars.
94
+
95
+ ### 6. Lua scripts reject non-numeric ARGV with `redis.error_reply`
96
+
97
+ **What changed.** Defensive `tonumber(x) ~= nil` checks were added at the top of every Lua script (`shared/scripts.js` CLEANUP/COUNT/COUNT_DEDUP/LIST, `redis/groups.js` JOIN, `redis/presence.js` JOIN/LEAVE, `redis/ratelimit.js` CONSUME/BAN, `redis/replay.js` PUBLISH, `redis/replay-stream.js` IDMP_PUBLISH/PUBLISH). Pre-fix, bypassing the JS validation layer (direct `redis.eval`, poisoned hash entries) would crash the script.
98
+
99
+ **How to migrate.** No source change for code that uses the public JS APIs. Code that calls `redis.eval` against extension scripts directly must pass numeric ARGV strings (`'1700000000'`, not `'now'`). Poisoned hash entries with non-numeric `ts` fields are now treated as stale rather than crashing iteration.
100
+
101
+ ### 7. Redis `ConnectionError` no longer leaks the URL password
102
+
103
+ **What changed.** When `new Redis(url, ...)` throws (DNS failure, malformed URL, bad TLS), the resulting `ConnectionError` message previously included the raw connection string with embedded password. The redis client factory now redacts the password segment to `***` via the new `shared/sensitive.js#redactConnectionUrl(url)` helper before interpolating into the error.
104
+
105
+ **How to migrate.** No source change. Error-tracker hooks and log pipelines that searched for the raw connection string must update their queries; the redacted form is `redis://:***@host:port`. Other URL forms (no userinfo, no password, query string with `@`, path-segment colons) pass through untouched.
106
+
107
+ ### 8. Presence wire shape on `__presence:{topic}` migrated to `presence_state` / `presence_diff`
108
+
109
+ **What changed.** The `redis/presence` channel previously emitted `list` / `join` / `leave` / `updated` / `heartbeat` events. It now emits:
110
+
111
+ - `presence_state` (sent once on subscribe to a single connection): payload is `{[userKey]: data}` - a flat snapshot.
112
+ - `presence_diff` (broadcast to topic subscribers, microtask-batched): payload is `{joins: {[key]: data}, leaves: {[key]: data}}`. Same-tick joins+leaves on the same key collapse to the latest op; updates appear as a `joins` entry with the new data.
113
+ - `heartbeat` is unchanged.
114
+
115
+ The public JS API on `createPresence` is unchanged (`join`, `leave`, `sync`, `list`, `count`, `metrics`, `clear`, `destroy`, `hooks` keep their signatures). The cross-instance Redis pub/sub envelope on `presence:events:{topic}` is also unchanged. The `keyspaceNotifications: true` mode now emits an empty `presence_state` event (was an empty `list` event) on hash expiry.
116
+
117
+ **How to migrate.** Apps with a custom WebSocket client decoding presence frames must swap their decoder from the four legacy event names to `presence_state` / `presence_diff`:
118
+
119
+ ```js
120
+ // before
121
+ case 'list': setAll(payload); break;
122
+ case 'join': add(payload.key, payload.data); break;
123
+ case 'leave': remove(payload.key); break;
124
+ case 'updated': set(payload.key, payload.data); break;
125
+
126
+ // after
127
+ case 'presence_state':
128
+ state.clear();
129
+ for (const k of Object.keys(payload)) state.set(k, payload[k]);
130
+ break;
131
+ case 'presence_diff':
132
+ for (const k of Object.keys(payload.joins)) state.set(k, payload.joins[k]);
133
+ for (const k of Object.keys(payload.leaves)) state.delete(k);
134
+ break;
135
+ ```
136
+
137
+ Apps using `svelte-realtime` get the new shape automatically. Apps using only the adapter's bundled in-memory `createPresence` plugin are unaffected; that plugin already used the new shape.
138
+
139
+ ### 9. `replay()` end event data changed from `null` to `{ reqId }`
140
+
141
+ **What changed.** The end marker on `__replay:{topic}` now sends `{ reqId: undefined }` (or `{ reqId: 'some-id' }` when a correlation ID is passed) instead of `null`. The signature also accepts an optional `reqId` parameter: `replay(ws, topic, sinceSeq, platform, reqId?)`. A new `truncated` event is sent before replay messages when the buffer has been trimmed past the client's `sinceSeq`. (This change shipped in 0.4.0 but is repeated here because client-side decoders are commonly out of date.)
142
+
143
+ **How to migrate.** Update any client-side end-marker check from `data === null` to an object check. Handle or ignore the new `truncated` event in your replay store.
144
+
145
+ ### 10. Default `select` strips `__`-prefixed and sensitive keys on presence and cursor
146
+
147
+ **What changed.** The default `select` on `createPresence` and `createCursor` was the identity function in earlier 0.3.x. Since 0.4.0 it strips keys starting with `__` (e.g. `__subscriptions`, `remoteAddress`) and keys matching `/token|secret|password|auth|session|cookie|jwt|credential/i`. Repeated here because it remains the most common upgrade trip-hazard.
148
+
149
+ **How to migrate.** If you relied on any of these fields in presence or cursor data, pass an explicit `select` function:
150
+
151
+ ```js
152
+ createPresence(redis, { select: (ud) => ({ name: ud.name, role: ud.role, token: ud.token }) });
153
+ ```
154
+
155
+ ### 11. Presence `hooks` now includes `unsubscribe`
156
+
157
+ **What changed.** `presence.hooks` exports a `unsubscribe` hook in addition to `subscribe` and `close`. Required for correct single-topic leave when a client unsubscribes from a topic without disconnecting.
158
+
159
+ **How to migrate.** Update destructuring:
160
+
161
+ ```js
162
+ // before
163
+ export const { subscribe, close } = presence.hooks;
164
+
165
+ // after
166
+ export const { subscribe, unsubscribe, close } = presence.hooks;
167
+ ```
168
+
169
+ ### 12. `replay.publish()` storage failures throw `ReplayStorageError`
170
+
171
+ **What changed.** Storage failures in `publish()` (and `publishIdempotent` on the stream backend) now throw `ReplayStorageError` with the original error preserved on `.cause`. Pre-fix the underlying error propagated raw. Affects `redis/replay`, `redis/replay-stream`, and `postgres/replay`.
172
+
173
+ **How to migrate.** Any consumer that catches a specific underlying class (ioredis errors, `CircuitBrokenError`) must either catch `ReplayStorageError` and inspect `.cause`:
174
+
175
+ ```js
176
+ try {
177
+ await replay.publish(topic, event, data);
178
+ } catch (err) {
179
+ if (err instanceof ReplayStorageError) {
180
+ const root = err.cause;
181
+ }
182
+ throw err;
183
+ }
184
+ ```
185
+
186
+ Or set `localFanoutOnStorageFailure: true` on the factory to opt into a best-effort `platform.publish` fallback (pure live frames; durability is sacrificed):
187
+
188
+ ```js
189
+ createReplay(redis, { localFanoutOnStorageFailure: true });
190
+ ```
191
+
192
+ `publishIdempotent` always throws `ReplayStorageError` on storage failure even when this option is set, since silent fanout would break the exactly-once contract.
193
+
194
+ ### 13. `idempotency.acquire(key)` parameter renamed to `idempotencyKey`
195
+
196
+ **What changed.** The first argument of `idempotency.acquire(...)` and `idempotency.purge(...)` is now named `idempotencyKey` instead of `key`, matching the existing `idempotencyKey` option on the task runner.
197
+
198
+ **How to migrate.** Function-argument rename, transparent at call sites that pass positionally. Code that destructures the parameter name from a wrapper signature should update accordingly.
199
+
200
+ ### 14. `createCursor` defaults flipped to a 60Hz world-state tick
201
+
202
+ **What changed.** `throttle` defaults to `16` (was `50`) and `topicThrottle` defaults to `16` (was `0`). Out of the box the broadcast path is now a 60Hz world-state tick: each topic emits at most one bulk frame per 16ms window carrying the latest position for every cursor that moved.
203
+
204
+ **How to migrate.** No action for apps that want the new default. To restore previous behavior:
205
+
206
+ ```js
207
+ // previous "every update broadcasts immediately" behavior
208
+ createCursor(redis, { topicThrottle: 0 });
209
+
210
+ // previous 50ms per-cursor floor
211
+ createCursor(redis, { throttle: 50 });
212
+ ```
213
+
214
+ For high-density rooms (>200 active movers) raise `topicThrottle` to 33 (30Hz).
215
+
216
+ ### 15. Pub/sub bus auto-emits `degraded` / `recovered` system events
217
+
218
+ **What changed.** When `createPubSubBus` shares a circuit breaker with the rest of the extensions, the bus subscribes to the breaker and auto-emits `degraded` / `recovered` events on a configurable system topic (default `'__realtime'`). Replaces the manually wired `breaker.onStateChange` to `distributed.publish('__system', ...)` pattern that the README previously documented.
219
+
220
+ **How to migrate.** Remove any manual wiring of breaker state to a `__system` topic if you previously followed the README pattern. To disable auto-emission, set `systemChannel: null` or `false`. To use a different topic, set `systemChannel: 'my-status-topic'`. The `onDegraded` / `onRecovered` callbacks remain available for server-side reactions regardless.
221
+
222
+ ### 16. Sharded pub/sub bus and Redis Functions wrapper require Redis 7+
223
+
224
+ **What changed.** `createShardedBus` (uses SPUBLISH/SSUBSCRIBE) and `createFunctionLibrary` (uses Redis Functions) run `INFO server` on activate and throw on Redis < 7. No EVALSHA or PUBLISH fallback is provided.
225
+
226
+ **How to migrate.** On Redis 6 or older Valkey, use `createPubSubBus` instead of `createShardedBus` and use `redis.eval` directly instead of `createFunctionLibrary`. Upgrade Redis to 7+ to use the new modules.
227
+
228
+ ### 17. Peer dependency on `svelte-adapter-uws` raised to `^0.5.0-next.19`
229
+
230
+ **What changed.** The peerDep range moved from `>=0.4.0` (0.4.x) through `^0.5.0-next.6` to `^0.5.0-next.19`. `bus.wrap()` now binds adapter members (`maxPayloadLength`, `bufferedAmount`, `subscribe`, `unsubscribe`, `checkSubscribe`, `sendCoalesced`, `request`, `requestId`, `pressure`, `onPressure`, `onPublishRate`, `publishBatched`) unconditionally; an older adapter without these members crashes at wrap-construction time.
231
+
232
+ **How to migrate.** Upgrade the adapter first, then this package:
233
+
234
+ ```sh
235
+ npm install svelte-adapter-uws@latest svelte-adapter-uws-extensions@latest
236
+ ```
237
+
238
+ See the adapter's MIGRATION.md for adapter-side breaking changes.
239
+
240
+ ### 18. `redis/lock` Lua scripts moved to `shared/lease-scripts.js`
241
+
242
+ **What changed.** The lock module's `HEARTBEAT_SCRIPT` and `RELEASE_SCRIPT` are now imported from `shared/lease-scripts.js` under generic names (`LEASE_RENEW_SCRIPT`, `LEASE_RELEASE_SCRIPT`) so `redis/leader` can reuse them. Pure refactor, no behavior change.
243
+
244
+ **How to migrate.** No action required for callers using the public lock API. Internal-tooling code that imported the script constants by name from `redis/lock` must update its import path and rename:
245
+
246
+ ```js
247
+ // before
248
+ import { HEARTBEAT_SCRIPT, RELEASE_SCRIPT } from 'svelte-adapter-uws-extensions/redis/lock';
249
+
250
+ // after
251
+ import { LEASE_RENEW_SCRIPT, LEASE_RELEASE_SCRIPT } from 'svelte-adapter-uws-extensions/shared/lease-scripts';
252
+ ```
253
+
254
+ ### 19. Default `select` strips sensitive keys with one-time warnings
255
+
256
+ **What changed.** Presence and cursor warn once (per-process) if userData contains keys matching `/token|secret|password|auth|session|cookie|jwt|credential/i` even when not selected. Notify bridge warns when payload approaches Postgres ~8000 byte NOTIFY limit.
257
+
258
+ **How to migrate.** Treat the warnings as actionable: either rename the offending field, exclude it from upstream user data, or pass a custom `select` that intentionally retains it. Warnings do not throw or alter behavior.
259
+
260
+ ### 20. New parse-error counters
261
+
262
+ **What changed.** The Redis pub/sub bus and sharded pub/sub bus previously swallowed malformed envelopes silently on receive. They now bump a `pubsub_parse_errors_total` / `sharded_pubsub_parse_errors_total` counter so a stream of bad messages becomes observable in Prometheus. Not a code break per se, but alert thresholds that assumed zero parse errors must be updated.
263
+
264
+ **How to migrate.** Add the new counters to your dashboards next to the existing `notify_parse_errors_total` on `createNotifyBridge`. No source change.
265
+
266
+ ### 21. `JOIN_SCRIPT` in groups returns `[status, ...liveMembers]`
267
+
268
+ **What changed.** Internal change since 0.4.0, but affects anyone calling the Lua script directly. The wire contract on the public groups API is unchanged.
269
+
270
+ **How to migrate.** If you do not call `JOIN_SCRIPT` directly via `redis.eval`, no action. Direct callers must read the array form.
271
+
272
+ ## After upgrading
273
+
274
+ Run your test suite. Pay particular attention to:
275
+
276
+ - Custom WebSocket clients decoding `__presence:{topic}` and `__replay:{topic}` frames.
277
+ - Code that catches storage errors on `replay.publish()`.
278
+ - Postgres deployments that relied on `ws_*` table names.
279
+ - Redis deployments with versioned rate limiter keys (no action needed; old `ratelimit:*` keys expire on their own).
280
+ - Bus subscribers receiving envelopes larger than 1 MB.
281
+ - Tasks paths that pass long `idempotencyKey` strings (now capped at 256 chars).
282
+
283
+ Report regressions against the changelog entry the issue maps to.