@opengeni/sdk 0.11.0 → 0.15.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/README.md +34 -12
- package/dist/index.d.ts +658 -83
- package/dist/index.js +1043 -304
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/client.ts +1367 -352
- package/src/errors.ts +20 -1
- package/src/index.ts +80 -6
- package/src/proxy.ts +1 -1
- package/src/sse.ts +11 -8
- package/src/stream.ts +9 -2
- package/src/types.ts +892 -102
- package/src/workspace-control-stream.ts +101 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isAbortError, isRetryableStreamError, OpenGeniStreamError } from "./errors";
|
|
2
|
+
import { parseSseStream } from "./sse";
|
|
3
|
+
import type { StreamSessionEventsOptions } from "./stream";
|
|
4
|
+
import type { WorkspaceControlEvent } from "./types";
|
|
5
|
+
|
|
6
|
+
export type WorkspaceControlStreamTransport = {
|
|
7
|
+
/** The server replays every durable event after the cursor before going live. */
|
|
8
|
+
openStream: (
|
|
9
|
+
after: number,
|
|
10
|
+
signal: AbortSignal | undefined,
|
|
11
|
+
) => Promise<ReadableStream<Uint8Array>>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reconnecting workspace invalidation stream. Control revisions are monotonic
|
|
16
|
+
* but can begin above one after the one-way migration, so unlike conversation
|
|
17
|
+
* events this stream intentionally permits sparse sequence values.
|
|
18
|
+
*/
|
|
19
|
+
export async function* streamWorkspaceControlEvents(
|
|
20
|
+
transport: WorkspaceControlStreamTransport,
|
|
21
|
+
options: StreamSessionEventsOptions = {},
|
|
22
|
+
): AsyncGenerator<WorkspaceControlEvent, void, void> {
|
|
23
|
+
const signal = options.signal;
|
|
24
|
+
const reconnect = options.reconnect ?? true;
|
|
25
|
+
const baseDelayMs = options.reconnectDelayMs ?? 500;
|
|
26
|
+
const maxDelayMs = options.maxReconnectDelayMs ?? 10_000;
|
|
27
|
+
const maxAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
|
|
28
|
+
let cursor = options.after ?? 0;
|
|
29
|
+
let failures = 0;
|
|
30
|
+
let delayMs = baseDelayMs;
|
|
31
|
+
let everConnected = false;
|
|
32
|
+
|
|
33
|
+
for (;;) {
|
|
34
|
+
if (signal?.aborted) return;
|
|
35
|
+
options.onStateChange?.(everConnected || failures > 0 ? "reconnecting" : "connecting");
|
|
36
|
+
const cursorAtOpen = cursor;
|
|
37
|
+
try {
|
|
38
|
+
const body = await transport.openStream(cursor, signal);
|
|
39
|
+
everConnected = true;
|
|
40
|
+
failures = 0;
|
|
41
|
+
delayMs = baseDelayMs;
|
|
42
|
+
await options.beforeLive?.();
|
|
43
|
+
options.onStateChange?.("live");
|
|
44
|
+
for await (const message of parseSseStream(body)) {
|
|
45
|
+
if (signal?.aborted) return;
|
|
46
|
+
const event = parseWorkspaceControlEvent(message.data);
|
|
47
|
+
if (!event || event.sequence <= cursor) continue;
|
|
48
|
+
cursor = event.sequence;
|
|
49
|
+
yield event;
|
|
50
|
+
}
|
|
51
|
+
if (!reconnect) return;
|
|
52
|
+
if (cursor === cursorAtOpen) await sleep(baseDelayMs, signal);
|
|
53
|
+
continue;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (signal?.aborted || isAbortError(error)) return;
|
|
56
|
+
if (!reconnect || !isRetryableStreamError(error)) throw error;
|
|
57
|
+
failures += 1;
|
|
58
|
+
if (failures > maxAttempts) {
|
|
59
|
+
throw new OpenGeniStreamError(
|
|
60
|
+
`workspace control stream gave up after ${maxAttempts} reconnect attempts: ${
|
|
61
|
+
error instanceof Error ? error.message : String(error)
|
|
62
|
+
}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await sleep(delayMs, signal);
|
|
67
|
+
delayMs = Math.min(Math.max(delayMs * 2, baseDelayMs), maxDelayMs);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseWorkspaceControlEvent(data: string): WorkspaceControlEvent | null {
|
|
72
|
+
let value: unknown;
|
|
73
|
+
try {
|
|
74
|
+
value = JSON.parse(data);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
typeof value !== "object" ||
|
|
80
|
+
value === null ||
|
|
81
|
+
(value as { type?: unknown }).type !== "workspace.control.changed" ||
|
|
82
|
+
typeof (value as { id?: unknown }).id !== "string" ||
|
|
83
|
+
typeof (value as { sequence?: unknown }).sequence !== "number"
|
|
84
|
+
) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return value as WorkspaceControlEvent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function sleep(delayMs: number, signal: AbortSignal | undefined): Promise<void> {
|
|
91
|
+
if (signal?.aborted || delayMs <= 0) return;
|
|
92
|
+
await new Promise<void>((resolve) => {
|
|
93
|
+
const timer = setTimeout(done, delayMs);
|
|
94
|
+
function done() {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
signal?.removeEventListener("abort", done);
|
|
97
|
+
resolve();
|
|
98
|
+
}
|
|
99
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
100
|
+
});
|
|
101
|
+
}
|