@dokebi/sse 0.4.0 → 0.4.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/AI.md +32 -18
- package/README.md +3 -2
- package/dist/browser/sse-client.js +88 -87
- package/dist/client/index.js +24 -5
- package/dist/client/react.js +3 -1
- package/dist/errors.d.ts +6 -1
- package/dist/errors.js +10 -1
- package/dist/gateway.d.ts +11 -2
- package/dist/gateway.js +294 -56
- package/dist/options.d.ts +30 -0
- package/dist/options.js +20 -6
- package/dist/plugin.js +34 -6
- package/dist/retry.d.ts +1 -0
- package/dist/retry.js +25 -0
- package/dist/transport.js +6 -3
- package/dist/types.d.ts +33 -4
- package/options.schema.json +67 -0
- package/package.json +11 -11
package/dist/options.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineOptions, option as o } from '@dokebi/core';
|
|
2
2
|
import { SseError } from './errors.js';
|
|
3
|
-
const int = (minimum, maximum, fallback, description) => o.integer({ minimum, maximum, default: fallback, description });
|
|
3
|
+
const int = (minimum, maximum, fallback, description, more = {}) => o.integer({ minimum, maximum, default: fallback, description, ...more });
|
|
4
|
+
/** D-0154: applied while the app runs (read per stream or per event; the ping timer restarts). */
|
|
5
|
+
const live = { reloadable: true };
|
|
4
6
|
/*
|
|
5
7
|
* Options of dokebi-sse (D-0063 §3): one declaration is the parser and, at build time,
|
|
6
8
|
* options.schema.json.
|
|
@@ -22,14 +24,26 @@ export const options = defineOptions({
|
|
|
22
24
|
default: false,
|
|
23
25
|
description: 'Start in a node:cluster worker without a nats bus (a deliberate single-worker cluster). Otherwise that start fails with LOCAL_IN_CLUSTER. The bus driver is read from dokebi-event-bus.buses in the application config.',
|
|
24
26
|
}),
|
|
25
|
-
retryMs: int(100, 3_600_000, 3000, "The retry: hint (browser reconnect delay), also the browser client's minimum backoff."),
|
|
26
|
-
heartbeatMs: int(10, 3_600_000, 15000, "How often a ': ping' comment is written to every stream. Proxy idle timeouts must be longer."),
|
|
27
|
-
maxConnections: int(1, 1_000_000, 1024, 'Open streams per process (per cluster worker); more get 503 CAPACITY.'),
|
|
28
|
-
maxBufferedBytes: int(1, 67_108_864, 1_048_576, 'Bytes a stream may have waiting to be sent; past it the stream is ended (warn SLOW_CONSUMER).'),
|
|
27
|
+
retryMs: int(100, 3_600_000, 3000, "The retry: hint (browser reconnect delay), also the browser client's minimum backoff.", live),
|
|
28
|
+
heartbeatMs: int(10, 3_600_000, 15000, "How often a ': ping' comment is written to every stream. Proxy idle timeouts must be longer.", live),
|
|
29
|
+
maxConnections: int(1, 1_000_000, 1024, 'Open streams per process (per cluster worker); more get 503 CAPACITY.', live),
|
|
30
|
+
maxBufferedBytes: int(1, 67_108_864, 1_048_576, 'Bytes a stream may have waiting to be sent; past it the stream is ended (warn SLOW_CONSUMER).', live),
|
|
29
31
|
maxRooms: int(0, 1024, 32, 'Rooms one stream may be in.'),
|
|
30
|
-
maxEventBytes: int(1, 16_777_216, 65_536, 'Largest JSON data of one event in bytes (EVENT_TOO_LARGE). Keep it below the bus maxPayloadBytes.'),
|
|
32
|
+
maxEventBytes: int(1, 16_777_216, 65_536, 'Largest JSON data of one event in bytes (EVENT_TOO_LARGE). Keep it below the bus maxPayloadBytes.', live),
|
|
31
33
|
shutdownTimeoutMs: int(1, 600_000, 2000, 'How long drain waits for streams to end after dokebi-restart before cutting them.'),
|
|
32
34
|
closeTimeoutMs: o.alias('shutdownTimeoutMs'),
|
|
35
|
+
endTimeoutMs: int(1, 600_000, 5000, 'How long a stream the server ended (stream.close(), revoked authorization, drain) may take to send its waiting bytes before it is destroyed (warn STREAM_END_TIMEOUT); frees the slot of a client that stopped reading.'),
|
|
36
|
+
busRetryAfterMs: o.object({
|
|
37
|
+
busy: int(1000, 3_600_000, 1000, 'Retry-After (ms, sent in whole seconds) of the 503 SSE_BUS_BUSY: the bus refused a publication at its pending limit.'),
|
|
38
|
+
unavailable: int(1000, 3_600_000, 5000, 'Retry-After (ms, sent in whole seconds) of the 503 SSE_BUS_UNAVAILABLE: the bus or its NATS connection is down or draining.'),
|
|
39
|
+
}, {
|
|
40
|
+
default: { busy: 1000, unavailable: 5000 },
|
|
41
|
+
description: 'Retry-After of the retryable 503 a route answers when a publish fails for a transient bus reason (D-0107); size it near how long the bus takes to recover.',
|
|
42
|
+
}),
|
|
43
|
+
beforeStreamTimeoutMs: int(100, 600_000, 5000, 'The longest wait for the work that must finish before a stream starts (the session save and auth cookies, D-0089); slower refuses the stream with 500 COMMIT_FAILED (D-0114).'),
|
|
44
|
+
reloadRetryMs: int(100, 86_400_000, 60_000, 'The retry: sent with dokebi-reload (a route whose version changed): a client that ignores the reload reconnects this slowly (D-0114).'),
|
|
45
|
+
busWatchIntervalMs: int(100, 60_000, 1000, 'How often the event bus state is read for the health check and its warn/info log; a bus outage shows within this long (D-0114).'),
|
|
46
|
+
logRepeatWindowMs: int(1000, 3_600_000, 10_000, 'Window of the repeated stream failure logs (a failed before-stream step, a failed write) in ms: the first is logged in full, the rest of the window are counted (D-0114).'),
|
|
33
47
|
});
|
|
34
48
|
/**
|
|
35
49
|
* Validates the plugin options (no accessors or Proxies are evaluated) and fills in defaults.
|
package/dist/plugin.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { definePlugin, optionalDependency, providerDependency, } from '@dokebi/core';
|
|
2
|
-
// Always installed with this plugin (peer, required in the manifest): the provider's own file
|
|
3
|
-
// holds the name, package and range (D-0063 §6).
|
|
4
|
-
import { dependency as web } from '@dokebi/web/dependency';
|
|
5
2
|
import { realtime } from '@dokebi/web/points';
|
|
6
3
|
import { SseError } from './errors.js';
|
|
7
4
|
import { Gateway } from './gateway.js';
|
|
8
5
|
import { options as declaration } from './options.js';
|
|
9
6
|
import { sseTransport } from './transport.js';
|
|
7
|
+
/**
|
|
8
|
+
* dokebi-web 0.4.3 or later: the shared before-hijack steps (`@dokebi/web/points`). A local
|
|
9
|
+
* descriptor instead of '@dokebi/web/dependency' (whose range stays ^0.4.0), as in dokebi-auth.
|
|
10
|
+
*/
|
|
11
|
+
const web = providerDependency({
|
|
12
|
+
name: 'dokebi-web',
|
|
13
|
+
version: '^0.4.3',
|
|
14
|
+
packageName: '@dokebi/web',
|
|
15
|
+
load: () => import('@dokebi/web'),
|
|
16
|
+
});
|
|
10
17
|
/** Only when `bus` is set; created from `app.config['dokebi-event-bus']` when not registered. */
|
|
11
18
|
const events = providerDependency({
|
|
12
19
|
name: 'dokebi-event-bus',
|
|
@@ -26,6 +33,18 @@ function configuredDriver(config, bus) {
|
|
|
26
33
|
const driver = value?.driver ?? value?.transport;
|
|
27
34
|
return typeof driver === 'string' ? driver : undefined;
|
|
28
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The maxPayloadBytes of a bus as the application config says (default 1048576 when the bus is
|
|
38
|
+
* configured without it); undefined when the bus options were given in code only.
|
|
39
|
+
*/
|
|
40
|
+
function configuredPayloadBytes(config, bus) {
|
|
41
|
+
const options = config?.['dokebi-event-bus'];
|
|
42
|
+
const value = options?.buses?.[bus];
|
|
43
|
+
if (value === undefined || value === null || typeof value !== 'object')
|
|
44
|
+
return undefined;
|
|
45
|
+
const bytes = value.maxPayloadBytes;
|
|
46
|
+
return bytes === undefined ? 1_048_576 : typeof bytes === 'number' ? bytes : undefined;
|
|
47
|
+
}
|
|
29
48
|
class SseRuntime {
|
|
30
49
|
options;
|
|
31
50
|
#gateway;
|
|
@@ -57,7 +76,11 @@ class SseRuntime {
|
|
|
57
76
|
const bus = options.eventBus === undefined
|
|
58
77
|
? undefined
|
|
59
78
|
: context.getDependency(events).bus(options.eventBus);
|
|
60
|
-
const gateway = new Gateway(context, context.getDependency(web), options, bus, context.getOptionalDependency(health)
|
|
79
|
+
const gateway = new Gateway(context, context.getDependency(web), options, bus, context.getOptionalDependency(health),
|
|
80
|
+
// Read at each check: the bus limit reloads (D-0154).
|
|
81
|
+
options.eventBus === undefined
|
|
82
|
+
? undefined
|
|
83
|
+
: () => configuredPayloadBytes(context.app.config, options.eventBus));
|
|
61
84
|
context.defer(() => gateway.close());
|
|
62
85
|
this.#gateway = gateway;
|
|
63
86
|
await gateway.subscribe();
|
|
@@ -74,6 +97,11 @@ class SseRuntime {
|
|
|
74
97
|
start() {
|
|
75
98
|
this.#gateway?.start();
|
|
76
99
|
}
|
|
100
|
+
/** D-0154: the stream limits and the ping interval (see Gateway.reconfigure). */
|
|
101
|
+
reload({ options }) {
|
|
102
|
+
const next = Object.freeze({ ...options, eventBus: options.eventBus });
|
|
103
|
+
return { apply: () => this.#gateway?.reconfigure(next) };
|
|
104
|
+
}
|
|
77
105
|
async drain(context) {
|
|
78
106
|
await this.#gateway?.drain(context.drainSignal);
|
|
79
107
|
}
|
|
@@ -81,8 +109,8 @@ class SseRuntime {
|
|
|
81
109
|
/** Factory input: `{ app, options? }`. Options default to `app.config['dokebi-sse']`. */
|
|
82
110
|
export const createPlugin = definePlugin({
|
|
83
111
|
name: 'dokebi-sse',
|
|
84
|
-
version: '0.4.
|
|
85
|
-
core: '^0.4.
|
|
112
|
+
version: '0.4.2',
|
|
113
|
+
core: '^0.4.2',
|
|
86
114
|
// The declaration (not parseOptions) so core logs OPTION_DEPRECATED for old keys (bus, closeTimeoutMs).
|
|
87
115
|
options: declaration,
|
|
88
116
|
contributes: [realtime],
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function transientBusFailure(cause: unknown): 'busy' | 'unavailable' | undefined;
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which failures of the event bus may pass when the request is retried (D-0107 §6): the bus
|
|
3
|
+
* refused at its pending limit (`busy`), or the bus or its NATS connection is down, closed or
|
|
4
|
+
* draining (`unavailable`). Recognised by `plugin` and `code` (another copy of the package
|
|
5
|
+
* counts). Anything else (an invalid payload or event name) is a defect and answers 500.
|
|
6
|
+
*/
|
|
7
|
+
const BUSY = new Set(['OVERLOADED']);
|
|
8
|
+
const UNAVAILABLE = new Set([
|
|
9
|
+
'TRANSPORT_FAILED',
|
|
10
|
+
'DEPENDENCY_UNAVAILABLE',
|
|
11
|
+
'BUS_DRAINING',
|
|
12
|
+
'CLOSED',
|
|
13
|
+
]);
|
|
14
|
+
export function transientBusFailure(cause) {
|
|
15
|
+
if (cause === null || typeof cause !== 'object')
|
|
16
|
+
return undefined;
|
|
17
|
+
const { plugin, code } = cause;
|
|
18
|
+
if (plugin !== 'dokebi-event-bus' || typeof code !== 'string')
|
|
19
|
+
return undefined;
|
|
20
|
+
if (BUSY.has(code))
|
|
21
|
+
return 'busy';
|
|
22
|
+
if (UNAVAILABLE.has(code))
|
|
23
|
+
return 'unavailable';
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
package/dist/transport.js
CHANGED
|
@@ -21,7 +21,10 @@ export function sseTransport(api, logger) {
|
|
|
21
21
|
},
|
|
22
22
|
});
|
|
23
23
|
if (logger.isEnabled('debug'))
|
|
24
|
-
logger.log('debug', 'SSE route mounted by the realtime point', {
|
|
24
|
+
logger.log('debug', 'SSE route mounted by the realtime point', {
|
|
25
|
+
path: scope.path,
|
|
26
|
+
routeId: typeof route.config?.id === 'string' ? route.config.id : null,
|
|
27
|
+
});
|
|
25
28
|
},
|
|
26
29
|
handle(namespace) {
|
|
27
30
|
if (typeof namespace !== 'string' || !NAMESPACE.test(namespace))
|
|
@@ -29,7 +32,7 @@ export function sseTransport(api, logger) {
|
|
|
29
32
|
const prefix = `${namespace}:`;
|
|
30
33
|
return Object.freeze({
|
|
31
34
|
namespace,
|
|
32
|
-
publish: (target, event, data) => {
|
|
35
|
+
publish: (target, event, data, options) => {
|
|
33
36
|
if (target === null || typeof target !== 'object')
|
|
34
37
|
return Promise.reject(new SseError('INVALID_TARGET', 'A namespace target is { tenant?, member?, room? }.'));
|
|
35
38
|
const { tenant, member, room } = target;
|
|
@@ -37,7 +40,7 @@ export function sseTransport(api, logger) {
|
|
|
37
40
|
...(tenant === undefined ? { tenantPrefix: prefix } : { tenant: prefix + tenant }),
|
|
38
41
|
...(member === undefined ? {} : { member }),
|
|
39
42
|
...(room === undefined ? {} : { room }),
|
|
40
|
-
}, event, data);
|
|
43
|
+
}, event, data, options);
|
|
41
44
|
},
|
|
42
45
|
});
|
|
43
46
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { Awaitable } from '@dokebi/core';
|
|
|
2
2
|
import type { FastifyRequest } from 'fastify';
|
|
3
3
|
/** Who a stream belongs to, as `authenticate` decided. Extra fields are kept on `stream.identity`. */
|
|
4
4
|
export interface SseIdentity {
|
|
5
|
+
/** Local renewable authorization, e.g. request.auth.live(); never published on the bus. */
|
|
6
|
+
readonly authorization?: {
|
|
7
|
+
valid(): boolean;
|
|
8
|
+
subscribe(invalidated: () => void): () => void;
|
|
9
|
+
};
|
|
5
10
|
readonly tenant: string;
|
|
6
11
|
readonly member: string;
|
|
7
12
|
/** Rooms the stream starts in (counted against maxRooms). */
|
|
@@ -47,18 +52,33 @@ export interface SseStream<TIdentity extends SseIdentity = SseIdentity> {
|
|
|
47
52
|
readonly closed: boolean;
|
|
48
53
|
/**
|
|
49
54
|
* Sends one event to this client only. False when the stream is closed (or was just ended as a
|
|
50
|
-
* slow consumer). Throws INVALID_EVENT, INVALID_DATA or
|
|
55
|
+
* slow consumer). Throws INVALID_EVENT, INVALID_DATA, EVENT_TOO_LARGE or INVALID_EVENT_ID.
|
|
51
56
|
*/
|
|
52
|
-
send(event: string, data?: unknown): boolean;
|
|
57
|
+
send(event: string, data?: unknown, options?: SseSendOptions): boolean;
|
|
53
58
|
/** Joins a room of this stream's tenant (this connection only). Throws ROOM_LIMIT, INVALID_ROOM. */
|
|
54
59
|
join(room: string): void;
|
|
55
60
|
leave(room: string): void;
|
|
56
61
|
/** Ends the stream (`SERVER_CLOSED`). The browser's EventSource reconnects after `retry`. */
|
|
57
62
|
close(): void;
|
|
58
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The route config of an SSE route (D-0140): only `id` for now. It becomes the Fastify route's
|
|
66
|
+
* `config`, so `dokebi describe` lists it as `routes[].id`, per-route hooks of dokebi-web
|
|
67
|
+
* (`web.routeHook`, audit hooks) see it, and a request reads it as `request.routeOptions.config.id`.
|
|
68
|
+
*/
|
|
69
|
+
export interface SseRouteConfig {
|
|
70
|
+
/**
|
|
71
|
+
* The route id of dokebi-web (D-0077 A), for example `'live'` or `'API-12'`: 1 to 64 letters,
|
|
72
|
+
* digits, `.`, `_` or `-`, starting with a letter. Checked at start like a web route's
|
|
73
|
+
* (INVALID_ROUTE_CONFIG); the same id on two routes of one app fails it (DUPLICATE_ROUTE_ID).
|
|
74
|
+
*/
|
|
75
|
+
readonly id?: string;
|
|
76
|
+
}
|
|
59
77
|
export interface SseRoute<TIdentity extends SseIdentity = SseIdentity> {
|
|
60
78
|
/** The GET path of the stream. */
|
|
61
79
|
readonly path: string;
|
|
80
|
+
/** `{ id }` (D-0140); any other key is INVALID_ROUTE. */
|
|
81
|
+
readonly config?: SseRouteConfig;
|
|
62
82
|
/** Fastify route constraints, for example `{ dokebiApp: 'shop' }` of @dokebi/app. */
|
|
63
83
|
readonly constraints?: Readonly<Record<string, unknown>>;
|
|
64
84
|
/**
|
|
@@ -98,6 +118,15 @@ export interface SsePrefixTarget {
|
|
|
98
118
|
readonly member?: string;
|
|
99
119
|
readonly room?: string;
|
|
100
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The event id the client sends back as `Last-Event-ID` when it reconnects. Left out, the plugin
|
|
123
|
+
* numbers events in this process (unique, but meaningless to another worker). Give an id of your
|
|
124
|
+
* own durable order (a message's sequence in its channel) so a client that reconnects to any
|
|
125
|
+
* worker can be sent what it missed from the database: 1 to 64 of A-Z a-z 0-9 and -.
|
|
126
|
+
*/
|
|
127
|
+
export interface SseSendOptions {
|
|
128
|
+
readonly id?: string;
|
|
129
|
+
}
|
|
101
130
|
export interface SsePublishResult {
|
|
102
131
|
/** The event id every recipient sees as `lastEventId`. */
|
|
103
132
|
readonly id: string;
|
|
@@ -120,7 +149,7 @@ export interface SseApi {
|
|
|
120
149
|
* INVALID_TARGET, INVALID_EVENT, INVALID_DATA, EVENT_TOO_LARGE, BUS_UNAVAILABLE (nothing sent)
|
|
121
150
|
* or BUS_PUBLISH_FAILED (sent here, other processes unknown; the error has `id` and `local`).
|
|
122
151
|
*/
|
|
123
|
-
publish(target: SseTarget | SsePrefixTarget, event: string, data?: unknown): Promise<SsePublishResult>;
|
|
152
|
+
publish(target: SseTarget | SsePrefixTarget, event: string, data?: unknown, options?: SseSendOptions): Promise<SsePublishResult>;
|
|
124
153
|
/** Open streams of this process (worker) that match. */
|
|
125
154
|
connections(filter?: SseFilter): number;
|
|
126
155
|
/** The bus name, when publications cross processes. */
|
|
@@ -136,7 +165,7 @@ export interface SseNamespaceTarget {
|
|
|
136
165
|
export interface SseNamespaceHandle {
|
|
137
166
|
readonly namespace: string;
|
|
138
167
|
/** `publish` with the tenant (or every tenant) of this namespace. */
|
|
139
|
-
publish(target: SseNamespaceTarget, event: string, data?: unknown): Promise<SsePublishResult>;
|
|
168
|
+
publish(target: SseNamespaceTarget, event: string, data?: unknown, options?: SseSendOptions): Promise<SsePublishResult>;
|
|
140
169
|
}
|
|
141
170
|
/** The events the plugin itself sends; application event names may not start with `dokebi-`. */
|
|
142
171
|
export declare const RELOAD_EVENT = "dokebi-reload";
|
package/options.schema.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"type": "string",
|
|
13
13
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
|
|
14
14
|
"deprecated": true,
|
|
15
|
+
"x-dokebi-alias-of": "eventBus",
|
|
15
16
|
"description": "Deprecated name of eventBus; use that key."
|
|
16
17
|
},
|
|
17
18
|
"busEvent": {
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"minimum": 100,
|
|
32
33
|
"maximum": 3600000,
|
|
33
34
|
"default": 3000,
|
|
35
|
+
"x-dokebi-reloadable": true,
|
|
34
36
|
"description": "The retry: hint (browser reconnect delay), also the browser client's minimum backoff."
|
|
35
37
|
},
|
|
36
38
|
"heartbeatMs": {
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
"minimum": 10,
|
|
39
41
|
"maximum": 3600000,
|
|
40
42
|
"default": 15000,
|
|
43
|
+
"x-dokebi-reloadable": true,
|
|
41
44
|
"description": "How often a ': ping' comment is written to every stream. Proxy idle timeouts must be longer."
|
|
42
45
|
},
|
|
43
46
|
"maxConnections": {
|
|
@@ -45,6 +48,7 @@
|
|
|
45
48
|
"minimum": 1,
|
|
46
49
|
"maximum": 1000000,
|
|
47
50
|
"default": 1024,
|
|
51
|
+
"x-dokebi-reloadable": true,
|
|
48
52
|
"description": "Open streams per process (per cluster worker); more get 503 CAPACITY."
|
|
49
53
|
},
|
|
50
54
|
"maxBufferedBytes": {
|
|
@@ -52,6 +56,7 @@
|
|
|
52
56
|
"minimum": 1,
|
|
53
57
|
"maximum": 67108864,
|
|
54
58
|
"default": 1048576,
|
|
59
|
+
"x-dokebi-reloadable": true,
|
|
55
60
|
"description": "Bytes a stream may have waiting to be sent; past it the stream is ended (warn SLOW_CONSUMER)."
|
|
56
61
|
},
|
|
57
62
|
"maxRooms": {
|
|
@@ -66,6 +71,7 @@
|
|
|
66
71
|
"minimum": 1,
|
|
67
72
|
"maximum": 16777216,
|
|
68
73
|
"default": 65536,
|
|
74
|
+
"x-dokebi-reloadable": true,
|
|
69
75
|
"description": "Largest JSON data of one event in bytes (EVENT_TOO_LARGE). Keep it below the bus maxPayloadBytes."
|
|
70
76
|
},
|
|
71
77
|
"shutdownTimeoutMs": {
|
|
@@ -80,7 +86,68 @@
|
|
|
80
86
|
"minimum": 1,
|
|
81
87
|
"maximum": 600000,
|
|
82
88
|
"deprecated": true,
|
|
89
|
+
"x-dokebi-alias-of": "shutdownTimeoutMs",
|
|
83
90
|
"description": "Deprecated name of shutdownTimeoutMs; use that key."
|
|
91
|
+
},
|
|
92
|
+
"endTimeoutMs": {
|
|
93
|
+
"type": "integer",
|
|
94
|
+
"minimum": 1,
|
|
95
|
+
"maximum": 600000,
|
|
96
|
+
"default": 5000,
|
|
97
|
+
"description": "How long a stream the server ended (stream.close(), revoked authorization, drain) may take to send its waiting bytes before it is destroyed (warn STREAM_END_TIMEOUT); frees the slot of a client that stopped reading."
|
|
98
|
+
},
|
|
99
|
+
"busRetryAfterMs": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"additionalProperties": false,
|
|
102
|
+
"properties": {
|
|
103
|
+
"busy": {
|
|
104
|
+
"type": "integer",
|
|
105
|
+
"minimum": 1000,
|
|
106
|
+
"maximum": 3600000,
|
|
107
|
+
"default": 1000,
|
|
108
|
+
"description": "Retry-After (ms, sent in whole seconds) of the 503 SSE_BUS_BUSY: the bus refused a publication at its pending limit."
|
|
109
|
+
},
|
|
110
|
+
"unavailable": {
|
|
111
|
+
"type": "integer",
|
|
112
|
+
"minimum": 1000,
|
|
113
|
+
"maximum": 3600000,
|
|
114
|
+
"default": 5000,
|
|
115
|
+
"description": "Retry-After (ms, sent in whole seconds) of the 503 SSE_BUS_UNAVAILABLE: the bus or its NATS connection is down or draining."
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"default": {
|
|
119
|
+
"busy": 1000,
|
|
120
|
+
"unavailable": 5000
|
|
121
|
+
},
|
|
122
|
+
"description": "Retry-After of the retryable 503 a route answers when a publish fails for a transient bus reason (D-0107); size it near how long the bus takes to recover."
|
|
123
|
+
},
|
|
124
|
+
"beforeStreamTimeoutMs": {
|
|
125
|
+
"type": "integer",
|
|
126
|
+
"minimum": 100,
|
|
127
|
+
"maximum": 600000,
|
|
128
|
+
"default": 5000,
|
|
129
|
+
"description": "The longest wait for the work that must finish before a stream starts (the session save and auth cookies, D-0089); slower refuses the stream with 500 COMMIT_FAILED (D-0114)."
|
|
130
|
+
},
|
|
131
|
+
"reloadRetryMs": {
|
|
132
|
+
"type": "integer",
|
|
133
|
+
"minimum": 100,
|
|
134
|
+
"maximum": 86400000,
|
|
135
|
+
"default": 60000,
|
|
136
|
+
"description": "The retry: sent with dokebi-reload (a route whose version changed): a client that ignores the reload reconnects this slowly (D-0114)."
|
|
137
|
+
},
|
|
138
|
+
"busWatchIntervalMs": {
|
|
139
|
+
"type": "integer",
|
|
140
|
+
"minimum": 100,
|
|
141
|
+
"maximum": 60000,
|
|
142
|
+
"default": 1000,
|
|
143
|
+
"description": "How often the event bus state is read for the health check and its warn/info log; a bus outage shows within this long (D-0114)."
|
|
144
|
+
},
|
|
145
|
+
"logRepeatWindowMs": {
|
|
146
|
+
"type": "integer",
|
|
147
|
+
"minimum": 1000,
|
|
148
|
+
"maximum": 3600000,
|
|
149
|
+
"default": 10000,
|
|
150
|
+
"description": "Window of the repeated stream failure logs (a failed before-stream step, a failed write) in ms: the first is logged in full, the rest of the window are counted (D-0114)."
|
|
84
151
|
}
|
|
85
152
|
}
|
|
86
153
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dokebi/sse",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Server-sent events for @dokebi/web: authenticated streams, rooms, cross-worker fan-out over @dokebi/event-bus, and a browser client",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dokebi",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"test": "node -e \"require('node:fs').rmSync('.tmp/tests',{recursive:true,force:true})\" && tsc -p tsconfig.test.json && node --experimental-eventsource --test \".tmp/tests/tests/*.test.js\""
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@dokebi/core": "^0.4.
|
|
53
|
-
"@dokebi/web": "^0.4.
|
|
52
|
+
"@dokebi/core": "^0.4.2",
|
|
53
|
+
"@dokebi/web": "^0.4.3",
|
|
54
54
|
"@dokebi/event-bus": "^0.4.0",
|
|
55
55
|
"fastify": "^5.12.7",
|
|
56
56
|
"react": ">=18",
|
|
@@ -68,12 +68,12 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@dokebi/cluster": "0.4.
|
|
72
|
-
"@dokebi/config": "0.4.
|
|
73
|
-
"@dokebi/event-bus": "0.4.
|
|
74
|
-
"@dokebi/health": "0.4.
|
|
75
|
-
"@dokebi/nats": "0.4.
|
|
76
|
-
"@dokebi/web": "0.4.
|
|
71
|
+
"@dokebi/cluster": "0.4.1",
|
|
72
|
+
"@dokebi/config": "0.4.2",
|
|
73
|
+
"@dokebi/event-bus": "0.4.2",
|
|
74
|
+
"@dokebi/health": "0.4.1",
|
|
75
|
+
"@dokebi/nats": "0.4.1",
|
|
76
|
+
"@dokebi/web": "0.4.3",
|
|
77
77
|
"react": "19.3.0",
|
|
78
78
|
"react-test-renderer": "19.3.0",
|
|
79
79
|
"vite": "8.3.4",
|
|
@@ -82,11 +82,11 @@
|
|
|
82
82
|
"dokebi": {
|
|
83
83
|
"schemaVersion": 2,
|
|
84
84
|
"id": "dokebi-sse",
|
|
85
|
-
"core": "^0.4.
|
|
85
|
+
"core": "^0.4.2",
|
|
86
86
|
"dependencies": [
|
|
87
87
|
{
|
|
88
88
|
"name": "dokebi-web",
|
|
89
|
-
"version": "^0.4.
|
|
89
|
+
"version": "^0.4.3"
|
|
90
90
|
},
|
|
91
91
|
{
|
|
92
92
|
"name": "dokebi-event-bus",
|