@rimelabs/sdk 0.1.0-alpha.25 → 0.1.0-alpha.26
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/CHANGELOG.md +7 -0
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +6 -0
- package/dist/realtime/transport.js +84 -5
- package/docs/realtime.md +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.0-alpha.26](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.25...typescript-v0.1.0-alpha.26) (2026-10-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* expose realtime connection diagnostics across SDKs ([#116](https://github.com/rimelabs/rime-sdk/issues/116)) ([da50cf4](https://github.com/rimelabs/rime-sdk/commit/da50cf41b0ca4631c0c71e48ef31cd207ab5e713))
|
|
9
|
+
|
|
3
10
|
## [0.1.0-alpha.25](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.24...typescript-v0.1.0-alpha.25) (2026-10-28)
|
|
4
11
|
|
|
5
12
|
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
/** Optional connection diagnostics. These fields do not initiate retries. */
|
|
2
|
+
export interface RimeErrorOptions extends ErrorOptions {
|
|
3
|
+
/** Refused HTTP upgrade status, such as 503; null when unavailable. */
|
|
4
|
+
statusCode?: number | null;
|
|
5
|
+
/** Received WebSocket close code, such as 1013; null when unavailable. */
|
|
6
|
+
closeCode?: number | null;
|
|
7
|
+
/** Server-requested delay in seconds. Zero is valid; null means unavailable. */
|
|
8
|
+
retryAfterSeconds?: number | null;
|
|
9
|
+
}
|
|
1
10
|
export declare class RimeError extends Error {
|
|
2
11
|
readonly requestId: string | null;
|
|
3
|
-
|
|
12
|
+
readonly statusCode: number | null;
|
|
13
|
+
readonly closeCode: number | null;
|
|
14
|
+
readonly retryAfterSeconds: number | null;
|
|
15
|
+
constructor(message: string, requestId?: string | null, options?: RimeErrorOptions);
|
|
4
16
|
}
|
|
5
17
|
export declare class RimeAuthenticationError extends RimeError {
|
|
6
18
|
}
|
package/dist/errors.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
export class RimeError extends Error {
|
|
2
2
|
requestId;
|
|
3
|
+
statusCode;
|
|
4
|
+
closeCode;
|
|
5
|
+
retryAfterSeconds;
|
|
3
6
|
constructor(message, requestId = null, options) {
|
|
4
7
|
super(message, options);
|
|
5
8
|
this.name = new.target.name;
|
|
6
9
|
this.requestId = requestId;
|
|
10
|
+
this.statusCode = options?.statusCode ?? null;
|
|
11
|
+
this.closeCode = options?.closeCode ?? null;
|
|
12
|
+
this.retryAfterSeconds = options?.retryAfterSeconds ?? null;
|
|
7
13
|
}
|
|
8
14
|
}
|
|
9
15
|
export class RimeAuthenticationError extends RimeError {
|
|
@@ -1,7 +1,83 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import { RimeAuthenticationError, RimeResourceLimitError, RimeStreamError, RimeUnavailableError, } from "../errors.js";
|
|
3
3
|
import { Deferred } from "./async.js";
|
|
4
|
-
|
|
4
|
+
const months = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
|
|
5
|
+
const dayName = "(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)";
|
|
6
|
+
const monthName = `(?:${months.join("|")})`;
|
|
7
|
+
const clock = "([0-9]{2}):([0-9]{2}):([0-9]{2})";
|
|
8
|
+
const imfDate = new RegExp(`^${dayName}, ([0-9]{2}) (${monthName}) ([0-9]{4}) ${clock} GMT$`);
|
|
9
|
+
const rfc850Date = new RegExp(`^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), ([0-9]{2})-(${monthName})-([0-9]{2}) ${clock} GMT$`);
|
|
10
|
+
const asctimeDate = new RegExp(`^${dayName} (${monthName}) ([0-9]{2}| [0-9]) ${clock} ([0-9]{4})$`);
|
|
11
|
+
function httpDate(value, now) {
|
|
12
|
+
let parts = imfDate.exec(value);
|
|
13
|
+
let shortYear = false;
|
|
14
|
+
if (!parts) {
|
|
15
|
+
parts = rfc850Date.exec(value);
|
|
16
|
+
shortYear = parts !== null;
|
|
17
|
+
}
|
|
18
|
+
if (!parts) {
|
|
19
|
+
const legacy = asctimeDate.exec(value);
|
|
20
|
+
if (!legacy)
|
|
21
|
+
return null;
|
|
22
|
+
parts = [
|
|
23
|
+
legacy[0],
|
|
24
|
+
legacy[2],
|
|
25
|
+
legacy[1],
|
|
26
|
+
legacy[6],
|
|
27
|
+
legacy[3],
|
|
28
|
+
legacy[4],
|
|
29
|
+
legacy[5],
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
const day = Number(parts[1]);
|
|
33
|
+
const month = months.indexOf(parts[2]);
|
|
34
|
+
let year = Number(parts[3]);
|
|
35
|
+
const hour = Number(parts[4]);
|
|
36
|
+
const minute = Number(parts[5]);
|
|
37
|
+
const second = Number(parts[6]);
|
|
38
|
+
if (shortYear) {
|
|
39
|
+
const current = new Date(now);
|
|
40
|
+
const limit = [
|
|
41
|
+
current.getUTCFullYear() + 50,
|
|
42
|
+
current.getUTCMonth(),
|
|
43
|
+
current.getUTCDate(),
|
|
44
|
+
current.getUTCHours(),
|
|
45
|
+
current.getUTCMinutes(),
|
|
46
|
+
current.getUTCSeconds(),
|
|
47
|
+
];
|
|
48
|
+
year += Math.floor(limit[0] / 100) * 100;
|
|
49
|
+
const candidate = [year, month, day, hour, minute, second];
|
|
50
|
+
const difference = candidate.findIndex((part, index) => part !== limit[index]);
|
|
51
|
+
if (difference !== -1 && candidate[difference] > limit[difference])
|
|
52
|
+
year -= 100;
|
|
53
|
+
}
|
|
54
|
+
// Use setters so years 0000–0099 aren't silently changed to 1900–1999.
|
|
55
|
+
const date = new Date(0);
|
|
56
|
+
date.setUTCFullYear(year, month, day);
|
|
57
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
58
|
+
if (year < 1 ||
|
|
59
|
+
date.getUTCFullYear() !== year ||
|
|
60
|
+
date.getUTCMonth() !== month ||
|
|
61
|
+
date.getUTCDate() !== day ||
|
|
62
|
+
date.getUTCHours() !== hour ||
|
|
63
|
+
date.getUTCMinutes() !== minute ||
|
|
64
|
+
date.getUTCSeconds() !== second)
|
|
65
|
+
return null;
|
|
66
|
+
return date.getTime();
|
|
67
|
+
}
|
|
68
|
+
function retryAfter(values) {
|
|
69
|
+
if (values?.length !== 1 || values[0].length > 256)
|
|
70
|
+
return null;
|
|
71
|
+
const value = values[0].trim();
|
|
72
|
+
if (/^[0-9]+$/.test(value)) {
|
|
73
|
+
const seconds = Number(value);
|
|
74
|
+
return Number.isFinite(seconds) ? seconds : null;
|
|
75
|
+
}
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const date = httpDate(value, now);
|
|
78
|
+
return date === null ? null : Math.max(0, (date - now) / 1000);
|
|
79
|
+
}
|
|
80
|
+
function handshakeError(status, body, requestId, retryAfterSeconds) {
|
|
5
81
|
let errorType = RimeUnavailableError;
|
|
6
82
|
let reason = "Realtime endpoint rejected the connection";
|
|
7
83
|
if ([401, 403].includes(status)) {
|
|
@@ -35,7 +111,10 @@ function handshakeError(status, body, requestId) {
|
|
|
35
111
|
// Invalid response bodies do not change the HTTP error category.
|
|
36
112
|
}
|
|
37
113
|
}
|
|
38
|
-
return new errorType(message, requestId
|
|
114
|
+
return new errorType(message, requestId, {
|
|
115
|
+
statusCode: status,
|
|
116
|
+
retryAfterSeconds,
|
|
117
|
+
});
|
|
39
118
|
}
|
|
40
119
|
/** Private transport. The public declarations do not require ws types. */
|
|
41
120
|
export class RealtimeConnection {
|
|
@@ -74,7 +153,7 @@ export class RealtimeConnection {
|
|
|
74
153
|
clearTimeout(timer);
|
|
75
154
|
this.refusal = undefined;
|
|
76
155
|
const id = response.headers["x-request-id"];
|
|
77
|
-
this.failure(handshakeError(response.statusCode ?? 0, complete ? Buffer.concat(chunks) : Buffer.alloc(0), typeof id === "string" ? id : null));
|
|
156
|
+
this.failure(handshakeError(response.statusCode ?? 0, complete ? Buffer.concat(chunks) : Buffer.alloc(0), typeof id === "string" ? id : null, retryAfter(response.headersDistinct["retry-after"])));
|
|
78
157
|
response.destroy();
|
|
79
158
|
request.destroy();
|
|
80
159
|
this.socket.terminate();
|
|
@@ -104,10 +183,10 @@ export class RealtimeConnection {
|
|
|
104
183
|
}
|
|
105
184
|
this.onMessage(data.toString());
|
|
106
185
|
});
|
|
107
|
-
this.socket.on("close", () => {
|
|
186
|
+
this.socket.on("close", (code) => {
|
|
108
187
|
this.refusal?.();
|
|
109
188
|
this.ended.resolve();
|
|
110
|
-
this.failure(new RimeStreamError("Realtime connection closed; reconnect is not automatic"));
|
|
189
|
+
this.failure(new RimeStreamError("Realtime connection closed; reconnect is not automatic", null, { closeCode: code === 1005 || code === 1006 ? null : code }));
|
|
111
190
|
});
|
|
112
191
|
}
|
|
113
192
|
failure(error) {
|
package/docs/realtime.md
CHANGED
|
@@ -452,6 +452,20 @@ The voice examples report actual playback.
|
|
|
452
452
|
|
|
453
453
|
## Timeouts and failures
|
|
454
454
|
|
|
455
|
+
`RimeError` exposes `statusCode` for a refused HTTP upgrade, `closeCode` for a
|
|
456
|
+
received WebSocket close code, and `retryAfterSeconds` for the server's requested
|
|
457
|
+
delay. For example, a busy endpoint can return `statusCode=503` and
|
|
458
|
+
`retryAfterSeconds=2`. Missing values are `null`; zero is a valid delay. The
|
|
459
|
+
existing `requestId` retains the server's request ID on an HTTP refusal.
|
|
460
|
+
Invalid or duplicate `Retry-After` headers are ignored. HTTP dates become a delay
|
|
461
|
+
relative to the client clock, with past dates becoming zero.
|
|
462
|
+
|
|
463
|
+
Error categories stay the same. A WebSocket closure uses `RimeStreamError`;
|
|
464
|
+
no received close code means `closeCode=null`, including a lost connection or
|
|
465
|
+
an empty close frame. These fields require a release with structured connection
|
|
466
|
+
diagnostics. The SDK does not retry startup automatically. These fields do not
|
|
467
|
+
make replay of an accepted request safe.
|
|
468
|
+
|
|
455
469
|
Configure positive, finite seconds through `timeouts`. `connectS` bounds the
|
|
456
470
|
WebSocket handshake and initial session event. `readyS` bounds turn admission,
|
|
457
471
|
tool readiness, and waiting for generation to end before a playback report.
|