@dokebi/sse 0.1.0 → 0.4.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.
- package/AI.md +94 -78
- package/README.md +44 -40
- package/dist/errors.d.ts +6 -4
- package/dist/errors.js +24 -4
- package/dist/gateway.js +32 -26
- package/dist/install.d.ts +1 -1
- package/dist/install.js +7 -5
- package/dist/options.d.ts +18 -3
- package/dist/options.js +47 -59
- package/dist/plugin.js +40 -32
- package/dist/transport.d.ts +13 -0
- package/dist/transport.js +45 -0
- package/dist/types.d.ts +12 -0
- package/options.schema.json +16 -3
- package/package.json +39 -17
- package/dist/log.d.ts +0 -25
- package/dist/log.js +0 -102
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dokebi/sse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Server-sent events for @dokebi/web: authenticated streams, rooms, cross-worker fan-out over @dokebi/event-bus, and a browser client",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dokebi",
|
|
7
|
+
"dokebi-plugin",
|
|
8
|
+
"web",
|
|
9
|
+
"sse",
|
|
10
|
+
"realtime"
|
|
11
|
+
],
|
|
4
12
|
"private": false,
|
|
5
13
|
"type": "module",
|
|
6
|
-
"description": "Server-sent events for @dokebi/web: authenticated streams, rooms, cross-worker fan-out over @dokebi/event-bus, and a browser client",
|
|
7
14
|
"license": "MIT",
|
|
8
15
|
"main": "./dist/index.js",
|
|
9
16
|
"types": "./dist/index.d.ts",
|
|
@@ -37,14 +44,14 @@
|
|
|
37
44
|
"node": ">=24.0.0"
|
|
38
45
|
},
|
|
39
46
|
"scripts": {
|
|
40
|
-
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p tsconfig.browser.json && vite build -c vite.client.config.ts --logLevel warn",
|
|
47
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p tsconfig.browser.json && vite build -c vite.client.config.ts --logLevel warn && node -e \"import('./dist/options.js').then(async (m) => { const { renderOptionsSchema } = await import('@dokebi/core'); process.stdout.write(renderOptionsSchema(m.options)); })\" > options.schema.json",
|
|
41
48
|
"typecheck": "tsc -p tsconfig.test.json --noEmit",
|
|
42
|
-
"test": "tsc -p tsconfig.test.json && node --experimental-eventsource --test \".tmp/tests/tests/*.test.js\""
|
|
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\""
|
|
43
50
|
},
|
|
44
51
|
"peerDependencies": {
|
|
45
|
-
"@dokebi/core": "^0.
|
|
46
|
-
"@dokebi/web": "^0.
|
|
47
|
-
"@dokebi/event-bus": "^0.
|
|
52
|
+
"@dokebi/core": "^0.4.0",
|
|
53
|
+
"@dokebi/web": "^0.4.0",
|
|
54
|
+
"@dokebi/event-bus": "^0.4.0",
|
|
48
55
|
"fastify": "^5.12.7",
|
|
49
56
|
"react": ">=18",
|
|
50
57
|
"vue": "^3.3.0"
|
|
@@ -61,12 +68,12 @@
|
|
|
61
68
|
}
|
|
62
69
|
},
|
|
63
70
|
"devDependencies": {
|
|
64
|
-
"@dokebi/cluster": "0.
|
|
65
|
-
"@dokebi/config": "0.
|
|
66
|
-
"@dokebi/event-bus": "0.
|
|
67
|
-
"@dokebi/health": "0.
|
|
68
|
-
"@dokebi/nats": "0.
|
|
69
|
-
"@dokebi/web": "0.
|
|
71
|
+
"@dokebi/cluster": "0.4.0",
|
|
72
|
+
"@dokebi/config": "0.4.0",
|
|
73
|
+
"@dokebi/event-bus": "0.4.0",
|
|
74
|
+
"@dokebi/health": "0.4.0",
|
|
75
|
+
"@dokebi/nats": "0.4.0",
|
|
76
|
+
"@dokebi/web": "0.4.0",
|
|
70
77
|
"react": "19.3.0",
|
|
71
78
|
"react-test-renderer": "19.3.0",
|
|
72
79
|
"vite": "8.3.4",
|
|
@@ -75,27 +82,42 @@
|
|
|
75
82
|
"dokebi": {
|
|
76
83
|
"schemaVersion": 2,
|
|
77
84
|
"id": "dokebi-sse",
|
|
78
|
-
"core": "^0.
|
|
85
|
+
"core": "^0.4.0",
|
|
79
86
|
"dependencies": [
|
|
80
87
|
{
|
|
81
88
|
"name": "dokebi-web",
|
|
82
|
-
"version": "^0.
|
|
89
|
+
"version": "^0.4.0"
|
|
83
90
|
},
|
|
84
91
|
{
|
|
85
92
|
"name": "dokebi-event-bus",
|
|
86
|
-
"version": "^0.
|
|
93
|
+
"version": "^0.4.0",
|
|
87
94
|
"optional": true
|
|
88
95
|
},
|
|
89
96
|
{
|
|
90
97
|
"name": "dokebi-health",
|
|
91
|
-
"version": "^0.
|
|
98
|
+
"version": "^0.4.0",
|
|
92
99
|
"optional": true
|
|
93
100
|
}
|
|
94
101
|
],
|
|
102
|
+
"contributes": [
|
|
103
|
+
"dokebi-web:realtime"
|
|
104
|
+
],
|
|
95
105
|
"installModule": "dist/install.js",
|
|
96
106
|
"optionsSchema": "options.schema.json",
|
|
97
107
|
"documentation": "AI.md"
|
|
98
108
|
},
|
|
109
|
+
"dokebiCatalog": {
|
|
110
|
+
"kind": "plugin",
|
|
111
|
+
"summary": "Server-sent events (SSE) streams and rooms, delivery across workers, and a browser client",
|
|
112
|
+
"maturity": "preview",
|
|
113
|
+
"provides": [
|
|
114
|
+
"realtime.sse"
|
|
115
|
+
],
|
|
116
|
+
"doesNot": [
|
|
117
|
+
"No ACK, redelivery or retention (at most once)"
|
|
118
|
+
],
|
|
119
|
+
"recipes": []
|
|
120
|
+
},
|
|
99
121
|
"publishConfig": {
|
|
100
122
|
"access": "public",
|
|
101
123
|
"registry": "https://registry.npmjs.org/"
|
package/dist/log.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { LogFields, Logger, LogLevel } from '@dokebi/core';
|
|
2
|
-
/**
|
|
3
|
-
* The full text of a caught value for the `error` log field (D-0047): name, whole message and
|
|
4
|
-
* stack, then each `cause` the same way. Only credentials in URLs (`//user:password@`) are
|
|
5
|
-
* removed; a text over 16 KB is cut and marked.
|
|
6
|
-
*/
|
|
7
|
-
export declare function errorText(error: unknown): string;
|
|
8
|
-
/** A stable code of an error (hub, ws or errno code), or `fallback`. */
|
|
9
|
-
export declare function errorCode(error: unknown, fallback: string): string;
|
|
10
|
-
/**
|
|
11
|
-
* Logs a repeating event once per interval (D-0047): the first occurrence at once, the following
|
|
12
|
-
* ones of the same message and code (and `group`, when given) are counted and reported together
|
|
13
|
-
* (`repeated`) when the interval ends. `group` must come from a bounded set (e.g. registered
|
|
14
|
-
* handler types), never from client input, since each value keeps an entry for an interval. Hot paths (per message, per frame) can report every failure without a burst
|
|
15
|
-
* flooding the log. Field and error text are built only for lines that are written.
|
|
16
|
-
*/
|
|
17
|
-
export declare class LogThrottle {
|
|
18
|
-
#private;
|
|
19
|
-
readonly logger: Logger;
|
|
20
|
-
readonly intervalMs: number;
|
|
21
|
-
constructor(logger: Logger, intervalMs?: number);
|
|
22
|
-
report(level: LogLevel, message: string, code: string, error?: unknown, fields?: LogFields, group?: string): void;
|
|
23
|
-
/** Writes the counts still pending (on close), so a burst at shutdown is not lost. */
|
|
24
|
-
flush(): void;
|
|
25
|
-
}
|
package/dist/log.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
const CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
2
|
-
/**
|
|
3
|
-
* The full text of a caught value for the `error` log field (D-0047): name, whole message and
|
|
4
|
-
* stack, then each `cause` the same way. Only credentials in URLs (`//user:password@`) are
|
|
5
|
-
* removed; a text over 16 KB is cut and marked.
|
|
6
|
-
*/
|
|
7
|
-
export function errorText(error) {
|
|
8
|
-
const parts = [];
|
|
9
|
-
let current = error;
|
|
10
|
-
for (let depth = 0; current !== undefined && depth < 5; depth++) {
|
|
11
|
-
let text;
|
|
12
|
-
if (current instanceof Error)
|
|
13
|
-
text = current.stack ?? `${current.name}: ${current.message}`;
|
|
14
|
-
else if (typeof current === 'string')
|
|
15
|
-
text = current;
|
|
16
|
-
else {
|
|
17
|
-
try {
|
|
18
|
-
text = `NonError: ${JSON.stringify(current)}`;
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
text = `NonError: ${String(current)}`; // not serialisable (cycle, BigInt): its string form
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
parts.push(depth ? `Caused by: ${text}` : text);
|
|
25
|
-
current = current instanceof Error ? current.cause : undefined;
|
|
26
|
-
}
|
|
27
|
-
const text = parts.join('\n').replace(/\/\/[^\s/@]*@/g, '//***@');
|
|
28
|
-
return text.length > 16_384
|
|
29
|
-
? `${text.slice(0, 16_384)}… [truncated ${text.length - 16_384} characters]`
|
|
30
|
-
: text;
|
|
31
|
-
}
|
|
32
|
-
/** A stable code of an error (hub, ws or errno code), or `fallback`. */
|
|
33
|
-
export function errorCode(error, fallback) {
|
|
34
|
-
const code = error?.code;
|
|
35
|
-
return typeof code === 'string' && CODE.test(code) ? code : fallback;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Logs a repeating event once per interval (D-0047): the first occurrence at once, the following
|
|
39
|
-
* ones of the same message and code (and `group`, when given) are counted and reported together
|
|
40
|
-
* (`repeated`) when the interval ends. `group` must come from a bounded set (e.g. registered
|
|
41
|
-
* handler types), never from client input, since each value keeps an entry for an interval. Hot paths (per message, per frame) can report every failure without a burst
|
|
42
|
-
* flooding the log. Field and error text are built only for lines that are written.
|
|
43
|
-
*/
|
|
44
|
-
export class LogThrottle {
|
|
45
|
-
logger;
|
|
46
|
-
intervalMs;
|
|
47
|
-
#entries = new Map();
|
|
48
|
-
constructor(logger, intervalMs = 10_000) {
|
|
49
|
-
this.logger = logger;
|
|
50
|
-
this.intervalMs = intervalMs;
|
|
51
|
-
}
|
|
52
|
-
report(level, message, code, error, fields, group) {
|
|
53
|
-
if (!this.logger.isEnabled(level))
|
|
54
|
-
return;
|
|
55
|
-
const key = `${level}\u0000${code}\u0000${message}${group === undefined ? '' : `\u0000${group}`}`;
|
|
56
|
-
const entry = this.#entries.get(key);
|
|
57
|
-
if (entry) {
|
|
58
|
-
entry.count++;
|
|
59
|
-
entry.error = error;
|
|
60
|
-
entry.fields = fields;
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
this.#write(level, message, code, error, fields, 0);
|
|
64
|
-
const value = {
|
|
65
|
-
count: 0,
|
|
66
|
-
error: undefined,
|
|
67
|
-
fields: undefined,
|
|
68
|
-
timer: setTimeout(() => this.#elapsed(key, value, level, message, code), this.intervalMs),
|
|
69
|
-
};
|
|
70
|
-
value.timer.unref();
|
|
71
|
-
this.#entries.set(key, value);
|
|
72
|
-
}
|
|
73
|
-
#elapsed(key, entry, level, message, code) {
|
|
74
|
-
if (!entry.count) {
|
|
75
|
-
this.#entries.delete(key);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
this.#write(level, message, code, entry.error, entry.fields, entry.count);
|
|
79
|
-
entry.count = 0;
|
|
80
|
-
entry.timer = setTimeout(() => this.#elapsed(key, entry, level, message, code), this.intervalMs);
|
|
81
|
-
entry.timer.unref();
|
|
82
|
-
}
|
|
83
|
-
#write(level, message, code, error, fields, repeated) {
|
|
84
|
-
this.logger.log(level, message, {
|
|
85
|
-
...fields,
|
|
86
|
-
code,
|
|
87
|
-
...(error === undefined ? {} : { error: errorText(error) }),
|
|
88
|
-
...(repeated ? { repeated, intervalMs: this.intervalMs } : {}),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
/** Writes the counts still pending (on close), so a burst at shutdown is not lost. */
|
|
92
|
-
flush() {
|
|
93
|
-
for (const [key, entry] of this.#entries) {
|
|
94
|
-
clearTimeout(entry.timer);
|
|
95
|
-
if (entry.count) {
|
|
96
|
-
const [level, code, message] = key.split('\u0000');
|
|
97
|
-
this.#write(level, message, code, entry.error, entry.fields, entry.count);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
this.#entries.clear();
|
|
101
|
-
}
|
|
102
|
-
}
|