svelte-realtime 0.6.0-next.71 → 0.6.0-next.72
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/package.json +2 -2
- package/src/server/state.js +6 -0
- package/src/server/webhook-out.js +17 -2
- package/src/server/webhooks.js +47 -9
- package/src/server.d.ts +23 -9
- package/src/server.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-realtime",
|
|
3
|
-
"version": "0.6.0-next.
|
|
3
|
+
"version": "0.6.0-next.72",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"tag": "next"
|
|
6
6
|
},
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"peerDependencies": {
|
|
98
98
|
"@sveltejs/kit": "^2.0.0",
|
|
99
99
|
"svelte": "^4.0.0 || ^5.0.0",
|
|
100
|
-
"svelte-adapter-uws": "^0.6.0-next.
|
|
100
|
+
"svelte-adapter-uws": "^0.6.0-next.61"
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
103
|
"@playwright/test": "^1.59.1",
|
package/src/server/state.js
CHANGED
|
@@ -342,6 +342,12 @@ export const state = {
|
|
|
342
342
|
/** @type {any} Dead-letter store for undeliverable outbound webhooks (null = capture off). */
|
|
343
343
|
webhookDeadLetter: null,
|
|
344
344
|
|
|
345
|
+
/** @type {any} Retry budget for outbound webhooks (null = unrationed). */
|
|
346
|
+
webhookBudget: null,
|
|
347
|
+
|
|
348
|
+
/** @type {any} Endpoint-ejection breaker for outbound webhooks (null = no ejection). */
|
|
349
|
+
webhookBreaker: null,
|
|
350
|
+
|
|
345
351
|
/** @type {import('svelte-adapter-uws').Platform | null} Captured platform for dynamic derived recomputation */
|
|
346
352
|
derivedPlatform: null,
|
|
347
353
|
|
|
@@ -11,6 +11,21 @@ import { _IS_DEV } from './env.js';
|
|
|
11
11
|
/** Re-exported for the registration-time SSRF validation message in webhooks.js. */
|
|
12
12
|
export { redactUrl as _redactUrl };
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Assemble the optional delivery-control hooks for one webhook entry: the
|
|
16
|
+
* configured retry budget + endpoint-ejection breaker (wired via
|
|
17
|
+
* `configureWebhooks` / `realtime({ webhooks })`), keyed by the webhook's
|
|
18
|
+
* registration id so one endpoint's failures cannot trip or starve another.
|
|
19
|
+
* Returns undefined when neither is configured, so an unconfigured delivery is
|
|
20
|
+
* byte-identical to the bare call.
|
|
21
|
+
*/
|
|
22
|
+
function _webhookHooks(entry) {
|
|
23
|
+
const budget = state.webhookBudget;
|
|
24
|
+
const breaker = state.webhookBreaker;
|
|
25
|
+
if (!budget && !breaker) return undefined;
|
|
26
|
+
return { budget: budget || undefined, breaker: breaker || undefined, key: entry.id };
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
function _reportWebhookOutFailure(config, err, event, data, attempts) {
|
|
15
30
|
if (config.onFailure) {
|
|
16
31
|
try {
|
|
@@ -26,7 +41,7 @@ function _reportWebhookOutFailure(config, err, event, data, attempts) {
|
|
|
26
41
|
|
|
27
42
|
export async function _fireWebhookOut(entry, topic, event, data, platform) {
|
|
28
43
|
void platform; // accepted for call-site parity; delivery reads entry.config
|
|
29
|
-
const r = await deliverWebhook(entry.config, topic, event, data);
|
|
44
|
+
const r = await deliverWebhook(entry.config, topic, event, data, _webhookHooks(entry));
|
|
30
45
|
if (r.ok) return;
|
|
31
46
|
_reportWebhookOutFailure(entry.config, r.err, event, data, r.attempts);
|
|
32
47
|
const store = state.webhookDeadLetter;
|
|
@@ -47,7 +62,7 @@ export async function _fireWebhookOut(entry, topic, event, data, platform) {
|
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
export async function _replayWebhookOut(entry, topic, event, data) {
|
|
50
|
-
const r = await deliverWebhook(entry.config, topic, event, data);
|
|
65
|
+
const r = await deliverWebhook(entry.config, topic, event, data, _webhookHooks(entry));
|
|
51
66
|
if (r.ok) return { ok: true };
|
|
52
67
|
return { ok: false, error: String((r.err && r.err.message) || r.err || 'unknown') };
|
|
53
68
|
}
|
package/src/server/webhooks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { checkUrl } from 'svelte-adapter-uws/safe-url';
|
|
3
|
+
import { createRetryBudget, createWebhookBreaker } from 'svelte-adapter-uws/plugins/webhooks';
|
|
3
4
|
import { _redactUrl, _replayWebhookOut } from './webhook-out.js';
|
|
4
5
|
import { state, _webhookOutById } from './state.js';
|
|
5
6
|
import { createDeadLetterStore } from './dead-letter.js';
|
|
@@ -114,17 +115,29 @@ export const _webhooksOutboundRegister = function outbound(sources, config) {
|
|
|
114
115
|
};
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
|
-
* Configure the outbound-webhook plane.
|
|
118
|
-
* store that retains UNDELIVERABLE outbound-webhook events (retry-exhausted,
|
|
119
|
-
* SSRF-blocked, etc.) for admin inspection + replay, instead of reporting then
|
|
120
|
-
* dropping them. Off by default - a DLQ retains attacker-influenced event data,
|
|
121
|
-
* so it is opt-in.
|
|
118
|
+
* Configure the outbound-webhook plane.
|
|
122
119
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
120
|
+
* `deadLetter` enables the dead-letter store that retains UNDELIVERABLE
|
|
121
|
+
* outbound-webhook events (retry-exhausted, SSRF-blocked, etc.) for admin
|
|
122
|
+
* inspection + replay, instead of reporting then dropping them. Off by default -
|
|
123
|
+
* a DLQ retains attacker-influenced event data, so it is opt-in.
|
|
126
124
|
*
|
|
127
|
-
*
|
|
125
|
+
* `budget` rations retry AMPLIFICATION so a storm of failing deliveries to one
|
|
126
|
+
* endpoint cannot launch unbounded retry work (the first attempt of each
|
|
127
|
+
* delivery always proceeds). `breaker` ejects a persistently-failing endpoint:
|
|
128
|
+
* an open circuit fast-fails to the dead-letter store without touching the
|
|
129
|
+
* network, and heals after a probe succeeds. Both are keyed by the webhook's
|
|
130
|
+
* registration id, so one endpoint cannot trip or starve another. Off by
|
|
131
|
+
* default.
|
|
132
|
+
*
|
|
133
|
+
* Each option takes `true` for the built-in single-instance default (an
|
|
134
|
+
* in-memory store / an in-process token bucket / an in-process breaker), an
|
|
135
|
+
* instance for a cluster (e.g. a Redis dead-letter store, or a shared
|
|
136
|
+
* budget/breaker coordinator) for cross-instance durability, or `false`/`null`
|
|
137
|
+
* to disable. Also wired from `realtime({ webhooks: { deadLetter, budget,
|
|
138
|
+
* breaker } })`.
|
|
139
|
+
*
|
|
140
|
+
* @param {{ deadLetter?: boolean | object | null, budget?: boolean | object | null, breaker?: boolean | object | null }} [config]
|
|
128
141
|
*/
|
|
129
142
|
export function configureWebhooks(config = {}) {
|
|
130
143
|
if (config.deadLetter !== undefined) {
|
|
@@ -136,6 +149,31 @@ export function configureWebhooks(config = {}) {
|
|
|
136
149
|
state.webhookDeadLetter = null;
|
|
137
150
|
}
|
|
138
151
|
}
|
|
152
|
+
if (config.budget !== undefined) {
|
|
153
|
+
if (config.budget === true) {
|
|
154
|
+
if (!state.webhookBudget) state.webhookBudget = createRetryBudget();
|
|
155
|
+
} else if (config.budget && typeof config.budget === 'object') {
|
|
156
|
+
if (typeof config.budget.take !== 'function') {
|
|
157
|
+
throw new Error('[svelte-realtime] configureWebhooks({ budget }): a budget must have a take(key) method (or pass true for the built-in)');
|
|
158
|
+
}
|
|
159
|
+
state.webhookBudget = config.budget;
|
|
160
|
+
} else {
|
|
161
|
+
state.webhookBudget = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (config.breaker !== undefined) {
|
|
165
|
+
if (config.breaker === true) {
|
|
166
|
+
if (!state.webhookBreaker) state.webhookBreaker = createWebhookBreaker();
|
|
167
|
+
} else if (config.breaker && typeof config.breaker === 'object') {
|
|
168
|
+
const b = config.breaker;
|
|
169
|
+
if (typeof b.guard !== 'function' || typeof b.success !== 'function' || typeof b.failure !== 'function') {
|
|
170
|
+
throw new Error('[svelte-realtime] configureWebhooks({ breaker }): a breaker must have guard(key), success(key), and failure(err, key) methods (or pass true for the built-in)');
|
|
171
|
+
}
|
|
172
|
+
state.webhookBreaker = b;
|
|
173
|
+
} else {
|
|
174
|
+
state.webhookBreaker = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
139
177
|
}
|
|
140
178
|
|
|
141
179
|
/**
|
package/src/server.d.ts
CHANGED
|
@@ -4263,11 +4263,18 @@ export interface RealtimeConfig {
|
|
|
4263
4263
|
* that retains UNDELIVERABLE outbound-webhook events (retry-exhausted,
|
|
4264
4264
|
* SSRF-blocked, etc.) for admin inspection and replay, instead of reporting
|
|
4265
4265
|
* then dropping them. Off by default (a DLQ retains attacker-influenced event
|
|
4266
|
-
* data). `
|
|
4267
|
-
*
|
|
4268
|
-
*
|
|
4269
|
-
|
|
4270
|
-
|
|
4266
|
+
* data). `webhooks.budget` rations retry amplification and `webhooks.breaker`
|
|
4267
|
+
* ejects a persistently-failing endpoint (fast-fail to the DLQ, healing after
|
|
4268
|
+
* a probe succeeds), both keyed by the webhook's registration id. Each takes
|
|
4269
|
+
* `true` for the built-in single-instance default, an instance for a cluster
|
|
4270
|
+
* (a shared store / budget / breaker), or `false`/`null` to disable.
|
|
4271
|
+
* Equivalent to `configureWebhooks({ deadLetter, budget, breaker })`.
|
|
4272
|
+
*/
|
|
4273
|
+
webhooks?: {
|
|
4274
|
+
deadLetter?: boolean | DeadLetterStore | null;
|
|
4275
|
+
budget?: boolean | import('svelte-adapter-uws/plugins/webhooks').RetryBudget | null;
|
|
4276
|
+
breaker?: boolean | import('svelte-adapter-uws/plugins/webhooks').WebhookBreaker | null;
|
|
4277
|
+
} | null;
|
|
4271
4278
|
/**
|
|
4272
4279
|
* Opt-in protocol/contract version, an integer the app bumps only on a BREAKING
|
|
4273
4280
|
* wire/contract change. Declare it identically here and in client `configure({
|
|
@@ -4344,12 +4351,19 @@ export interface DeadLetterStore {
|
|
|
4344
4351
|
export function createDeadLetterStore(options?: { max?: number; ttlMs?: number }): DeadLetterStore;
|
|
4345
4352
|
|
|
4346
4353
|
/**
|
|
4347
|
-
* Configure the outbound-webhook plane. `deadLetter
|
|
4348
|
-
*
|
|
4349
|
-
*
|
|
4354
|
+
* Configure the outbound-webhook plane. `deadLetter` captures undeliverable
|
|
4355
|
+
* events; `budget` rations retry amplification; `breaker` ejects a
|
|
4356
|
+
* persistently-failing endpoint (fast-fail to the DLQ, healing after a probe
|
|
4357
|
+
* succeeds), keyed by the webhook's registration id. Each takes `true` for the
|
|
4358
|
+
* built-in single-instance default, an instance for a cluster (a shared store /
|
|
4359
|
+
* budget / breaker), or `false`/`null` to disable (the default). Also wired from
|
|
4350
4360
|
* `realtime({ webhooks })`.
|
|
4351
4361
|
*/
|
|
4352
|
-
export function configureWebhooks(config?: {
|
|
4362
|
+
export function configureWebhooks(config?: {
|
|
4363
|
+
deadLetter?: boolean | DeadLetterStore | null;
|
|
4364
|
+
budget?: boolean | import('svelte-adapter-uws/plugins/webhooks').RetryBudget | null;
|
|
4365
|
+
breaker?: boolean | import('svelte-adapter-uws/plugins/webhooks').WebhookBreaker | null;
|
|
4366
|
+
}): void;
|
|
4353
4367
|
|
|
4354
4368
|
/** The configured dead-letter store, or `null` when capture is off. */
|
|
4355
4369
|
export function getDeadLetter(): DeadLetterStore | null;
|
package/src/server.js
CHANGED
|
@@ -3536,8 +3536,9 @@ export function realtime(config) {
|
|
|
3536
3536
|
|
|
3537
3537
|
if (bus !== undefined) _setBus(bus);
|
|
3538
3538
|
if (leader !== undefined) configureCron({ leader });
|
|
3539
|
-
// Outbound-webhook plane: `webhooks.deadLetter`
|
|
3540
|
-
//
|
|
3539
|
+
// Outbound-webhook plane: `webhooks.deadLetter` retains + admin-replays
|
|
3540
|
+
// undeliverable events; `webhooks.budget`/`webhooks.breaker` ration retries
|
|
3541
|
+
// and eject a failing endpoint. Off unless configured.
|
|
3541
3542
|
if (webhooks !== undefined) configureWebhooks(webhooks);
|
|
3542
3543
|
// Multi-tenancy opt-in: a resolver mapping the server-trusted authenticated
|
|
3543
3544
|
// user (ws.getUserData()) to a tenant id auto-scopes every topic and key.
|