@opengeni/sdk 3.2.2 → 3.3.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 +21 -0
- package/dist/artifacts.js +4 -4
- package/dist/browser.js +2 -2
- package/dist/chat/index.js +15 -349
- package/dist/chat/index.js.map +1 -1
- package/dist/{chunk-JKG75TTU.js → chunk-BWAJPU5S.js} +12 -2
- package/dist/chunk-BWAJPU5S.js.map +1 -0
- package/dist/{chunk-PBVUAXSU.js → chunk-F4OXWFCU.js} +217 -7
- package/dist/chunk-F4OXWFCU.js.map +1 -0
- package/dist/{chunk-N2I422X4.js → chunk-HP7NVCAX.js} +3 -11
- package/dist/chunk-HP7NVCAX.js.map +1 -0
- package/dist/{chunk-JP62FZSI.js → chunk-MJWC6GTA.js} +2 -2
- package/dist/chunk-OV3POE62.js +359 -0
- package/dist/chunk-OV3POE62.js.map +1 -0
- package/dist/{chunk-PDU6EU2M.js → chunk-S5XTUA7L.js} +2 -2
- package/dist/{chunk-VPVMLM7Q.js → chunk-X5ITQVXE.js} +1 -1
- package/dist/chunk-X5ITQVXE.js.map +1 -0
- package/dist/client.d.ts +3 -0
- package/dist/core.js +3 -3
- package/dist/document-authority.js +3 -3
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/insights-usage.d.ts +1 -1
- package/dist/insights-usage.js +1 -1
- package/dist/site-ai.d.ts +35 -0
- package/dist/site-bridge-host.js +4 -3
- package/dist/site-bridge-host.js.map +1 -1
- package/dist/site-browser-runtime.gen.d.ts +1 -1
- package/dist/site-document.js +1 -1
- package/dist/site-document.js.map +1 -1
- package/dist/site.d.ts +4 -0
- package/dist/site.js +4 -3
- package/package.json +3 -3
- package/src/client.ts +19 -0
- package/src/insights-usage.ts +3 -0
- package/src/site-ai.ts +284 -0
- package/src/site-browser-runtime.gen.ts +1 -1
- package/src/site.ts +16 -5
- package/dist/chunk-JKG75TTU.js.map +0 -1
- package/dist/chunk-N2I422X4.js.map +0 -1
- package/dist/chunk-PBVUAXSU.js.map +0 -1
- package/dist/chunk-VPVMLM7Q.js.map +0 -1
- /package/dist/{chunk-JP62FZSI.js.map → chunk-MJWC6GTA.js.map} +0 -0
- /package/dist/{chunk-PDU6EU2M.js.map → chunk-S5XTUA7L.js.map} +0 -0
package/dist/site.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { type OpenGeniWorkspaceTools } from "./tools.js";
|
|
|
4
4
|
import { OpenGeniClient } from "./client.js";
|
|
5
5
|
import { type OpenGeniSiteData, type OpenGeniSiteFiles } from "./site-data.js";
|
|
6
6
|
import { type OpenGeniSiteSql } from "./site-sql.js";
|
|
7
|
+
import { type OpenGeniSiteAi } from "./site-ai.js";
|
|
8
|
+
export type { OpenGeniSiteAi, OpenGeniSiteAiRequest, OpenGeniSiteAiResult, OpenGeniSiteAiOptions, } from "./site-ai.js";
|
|
7
9
|
export { OpenGeniSiteSqlError, type OpenGeniSiteSql, type OpenGeniSiteSqlErrorCode, type OpenGeniSiteSqlResult, type OpenGeniSiteSqlValue, } from "./site-sql.js";
|
|
8
10
|
export { OpenGeniSiteDataError, SITE_DATA_PATH_PREFIX, type OpenGeniSiteData, type OpenGeniSiteDataCollection, type OpenGeniSiteDataErrorCode, type OpenGeniSiteDataFilter, type OpenGeniSiteDataListOptions, type OpenGeniSiteDataOperation, type OpenGeniSiteDataPage, type OpenGeniSiteDataRecord, type OpenGeniSiteDataRef, type OpenGeniSiteDataScalar, type OpenGeniSiteDataWriteResult, type OpenGeniSiteFile, type OpenGeniSiteFiles, } from "./site-data.js";
|
|
9
11
|
export { isSiteHttpRequest, serveSiteHttp, siteSessionPath, type SiteHttpRequest, } from "./site-http.js";
|
|
@@ -104,6 +106,8 @@ export type OpenGeniSiteBridgeResponseMessage = {
|
|
|
104
106
|
export type OpenGeniSiteClient = {
|
|
105
107
|
/** Ordinary SDK, including React session/composer methods and SSE streams. */
|
|
106
108
|
readonly client: OpenGeniClient;
|
|
109
|
+
/** Inline AI over ordinary Site sessions. No conversation component is required. */
|
|
110
|
+
readonly ai: OpenGeniSiteAi;
|
|
107
111
|
/** Host-bound routing alias, not a credential or a caller-selected tenant. */
|
|
108
112
|
readonly workspaceId: string;
|
|
109
113
|
/** Workspace-bound typed facade. The parent host owns workspace identity and credentials. */
|
package/dist/site.js
CHANGED
|
@@ -24,10 +24,11 @@ import {
|
|
|
24
24
|
sanitizeOpenGeniSiteToolInvokeRequest,
|
|
25
25
|
serveSiteHttp,
|
|
26
26
|
siteSessionPath
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-F4OXWFCU.js";
|
|
28
|
+
import "./chunk-OV3POE62.js";
|
|
28
29
|
import "./chunk-P7UWKORG.js";
|
|
29
|
-
import "./chunk-
|
|
30
|
-
import "./chunk-
|
|
30
|
+
import "./chunk-BWAJPU5S.js";
|
|
31
|
+
import "./chunk-X5ITQVXE.js";
|
|
31
32
|
import "./chunk-4AHR4KGA.js";
|
|
32
33
|
import "./chunk-KADZ5PGG.js";
|
|
33
34
|
import "./chunk-O7KANXC3.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Framework-agnostic TypeScript SDK for the Opengeni API: typed client, session lifecycle, SSE event streaming with reconnect + replay-by-sequence, and proxy re-streaming helpers.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -198,8 +198,8 @@
|
|
|
198
198
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@opengeni/connect": "3.
|
|
202
|
-
"@opengeni/contracts": "3.
|
|
201
|
+
"@opengeni/connect": "3.3.0",
|
|
202
|
+
"@opengeni/contracts": "3.3.0"
|
|
203
203
|
},
|
|
204
204
|
"engines": {
|
|
205
205
|
"node": ">=18"
|
package/src/client.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ComputerNativeCallRequest, ComputerNativeReceipt } from "@opengeni/contracts";
|
|
2
|
+
import type {
|
|
3
|
+
InsightsSiteUsageQuery,
|
|
4
|
+
InsightsSiteUsageResponse,
|
|
5
|
+
} from "@opengeni/contracts/insights-usage";
|
|
2
6
|
import type {
|
|
3
7
|
ClaudeSubscriptionAccountsResponse,
|
|
4
8
|
ClaudeSubscriptionAccount,
|
|
@@ -10332,6 +10336,21 @@ export class OpenGeniClient {
|
|
|
10332
10336
|
);
|
|
10333
10337
|
}
|
|
10334
10338
|
|
|
10339
|
+
/** Shared usage under the existing workspace access gate. */
|
|
10340
|
+
async getWorkspaceInsightsSiteUsage(
|
|
10341
|
+
workspaceId: string,
|
|
10342
|
+
options: Partial<InsightsSiteUsageQuery> = {},
|
|
10343
|
+
requestOptions: OpenGeniRequestOptions = {},
|
|
10344
|
+
): Promise<InsightsSiteUsageResponse> {
|
|
10345
|
+
return this.requestJson(
|
|
10346
|
+
"GET",
|
|
10347
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/insights/sites`,
|
|
10348
|
+
undefined,
|
|
10349
|
+
{ range: options.range ?? "month" },
|
|
10350
|
+
requestOptions,
|
|
10351
|
+
);
|
|
10352
|
+
}
|
|
10353
|
+
|
|
10335
10354
|
/** Shared usage under the existing workspace access gate. */
|
|
10336
10355
|
async getWorkspaceInsightsUsage(
|
|
10337
10356
|
workspaceId: string,
|
package/src/insights-usage.ts
CHANGED
package/src/site-ai.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { OpenGeniClient } from "./client";
|
|
2
|
+
import { ChatTurnFold, asRecord, type ChatTurnTerminal } from "./chat/fold";
|
|
3
|
+
import { sessionEventStreamCoveredThrough } from "./stream";
|
|
4
|
+
import type { CreateSessionRequest, Session, SessionEvent } from "./types";
|
|
5
|
+
|
|
6
|
+
/** App-authored input, unchanged: no built-in tasks, prompt templates or UI. */
|
|
7
|
+
export type OpenGeniSiteAiRequest = Pick<
|
|
8
|
+
CreateSessionRequest,
|
|
9
|
+
"initialMessage" | "idempotencyKey" | "model" | "reasoningEffort" | "latencyMode"
|
|
10
|
+
> & { initialMessage: string };
|
|
11
|
+
|
|
12
|
+
export type OpenGeniSiteAiResult = {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
turnId: string | null;
|
|
15
|
+
text: string;
|
|
16
|
+
status: "completed" | "pending" | "cancelled";
|
|
17
|
+
/** Cursor for subsequent output. Resuming after it returns a suffix, not earlier text. */
|
|
18
|
+
after: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type OpenGeniSiteAiOptions = {
|
|
22
|
+
/** Stops observing, not the underlying work. Use the ordinary session control API to cancel. */
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
/** Retain the handle before waiting for output, including when observation is interrupted. */
|
|
25
|
+
onSession?: (sessionId: string) => void;
|
|
26
|
+
/** Append-only answer text, folded by the same SDK implementation as embedded chat. */
|
|
27
|
+
onText?: (text: string) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type OpenGeniSiteAi = {
|
|
31
|
+
/** Starts an ordinary viewer-owned Site session and returns its first turn's output. */
|
|
32
|
+
run(
|
|
33
|
+
request: OpenGeniSiteAiRequest,
|
|
34
|
+
options?: OpenGeniSiteAiOptions,
|
|
35
|
+
): Promise<OpenGeniSiteAiResult>;
|
|
36
|
+
/** Observe without sending work. Omit `after` to replay the complete answer;
|
|
37
|
+
* with `after`, text/onText contain only subsequent output. Pass the retained
|
|
38
|
+
* turnId when resuming a pending turn, so another turn cannot settle it. */
|
|
39
|
+
result(
|
|
40
|
+
sessionId: string,
|
|
41
|
+
options?: OpenGeniSiteAiOptions & { after?: number; turnId?: string },
|
|
42
|
+
): Promise<OpenGeniSiteAiResult>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Native create, session state and replayable events; the host remains the authority. */
|
|
46
|
+
export function createOpenGeniSiteAi(
|
|
47
|
+
client: Pick<OpenGeniClient, "createSession" | "getSession" | "listEventPage" | "streamEvents">,
|
|
48
|
+
workspaceId: string,
|
|
49
|
+
): OpenGeniSiteAi {
|
|
50
|
+
const result: OpenGeniSiteAi["result"] = async (sessionId, options = {}) => {
|
|
51
|
+
options.signal?.throwIfAborted();
|
|
52
|
+
const upstream = new AbortController();
|
|
53
|
+
const onAbort = () => upstream.abort(options.signal?.reason);
|
|
54
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
55
|
+
const cutoff = options.after ?? 0;
|
|
56
|
+
let after = 0;
|
|
57
|
+
let text = "";
|
|
58
|
+
let fold =
|
|
59
|
+
options.turnId || cutoff === 0
|
|
60
|
+
? new ChatTurnFold(workspaceId, sessionId, options.turnId ?? null)
|
|
61
|
+
: null;
|
|
62
|
+
// Without a retained turn id, select the first turn after the cursor, as
|
|
63
|
+
// live observation does. Keep the prefix only until that turn is known.
|
|
64
|
+
let prefix: SessionEvent[] = [];
|
|
65
|
+
let terminal: ChatTurnTerminal | null = null;
|
|
66
|
+
const pending: { event: SessionEvent | null } = { event: null };
|
|
67
|
+
let stream: ReturnType<OpenGeniClient["streamEvents"]> | undefined;
|
|
68
|
+
let streamAbort: AbortController | undefined;
|
|
69
|
+
const closeStream = () => {
|
|
70
|
+
streamAbort?.abort();
|
|
71
|
+
// Some fetch implementations await request abort in iterator.return().
|
|
72
|
+
// Abort first, and never make result delivery depend on its unwinding.
|
|
73
|
+
if (stream) void Promise.resolve(stream.return(undefined)).catch(() => undefined);
|
|
74
|
+
stream = undefined;
|
|
75
|
+
streamAbort = undefined;
|
|
76
|
+
};
|
|
77
|
+
const onUpstreamAbort = () => streamAbort?.abort(upstream.signal.reason);
|
|
78
|
+
upstream.signal.addEventListener("abort", onUpstreamAbort);
|
|
79
|
+
const emit = (chunks: ReturnType<ChatTurnFold["push"]>["chunks"]) => {
|
|
80
|
+
for (const chunk of chunks)
|
|
81
|
+
if (chunk.type === "text") {
|
|
82
|
+
upstream.signal.throwIfAborted();
|
|
83
|
+
text += chunk.text;
|
|
84
|
+
options.onText?.(chunk.text);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const selectTurn = (turnId: string | null) => {
|
|
88
|
+
fold = new ChatTurnFold(workspaceId, sessionId, turnId);
|
|
89
|
+
for (const event of prefix) {
|
|
90
|
+
const step = fold.push(event);
|
|
91
|
+
if (step.terminal && step.terminal !== "pending") terminal = step.terminal;
|
|
92
|
+
}
|
|
93
|
+
prefix = [];
|
|
94
|
+
};
|
|
95
|
+
const consume = (event: SessionEvent) => {
|
|
96
|
+
const covered = eventCursor(event);
|
|
97
|
+
if (covered <= after) return;
|
|
98
|
+
after = covered;
|
|
99
|
+
if (!fold) {
|
|
100
|
+
if (covered <= cutoff) {
|
|
101
|
+
prefix.push(event);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!isTurnOutput(event) && !isWait(event)) return;
|
|
105
|
+
selectTurn(event.turnId ?? lastTurnId(prefix));
|
|
106
|
+
}
|
|
107
|
+
const current = fold!;
|
|
108
|
+
const sameTurn = !event.turnId || !current.turnId || event.turnId === current.turnId;
|
|
109
|
+
if (sameTurn && isWait(event)) {
|
|
110
|
+
if (!current.turnId && event.turnId) current.turnId = event.turnId;
|
|
111
|
+
if (event.type !== "session.status.changed") pending.event = event;
|
|
112
|
+
// A historical wait is not a settlement. Only fold it here when
|
|
113
|
+
// rebuilding text the caller already observed before their cursor.
|
|
114
|
+
if (covered > cutoff) return;
|
|
115
|
+
}
|
|
116
|
+
const step = current.push(event);
|
|
117
|
+
if (covered > cutoff) emit(step.chunks);
|
|
118
|
+
if (step.terminal && step.terminal !== "pending") terminal = step.terminal;
|
|
119
|
+
};
|
|
120
|
+
const reply = (status: ChatTurnTerminal): OpenGeniSiteAiResult => {
|
|
121
|
+
upstream.signal.throwIfAborted();
|
|
122
|
+
if (status === "failed") throw fold!.failureError();
|
|
123
|
+
return {
|
|
124
|
+
sessionId,
|
|
125
|
+
turnId: fold?.turnId ?? null,
|
|
126
|
+
text,
|
|
127
|
+
status:
|
|
128
|
+
status === "pending" ? "pending" : status === "cancelled" ? "cancelled" : "completed",
|
|
129
|
+
after: Math.max(cutoff, after),
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const readSession = () =>
|
|
133
|
+
observe(
|
|
134
|
+
() => client.getSession(workspaceId, sessionId, { fresh: true, signal: upstream.signal }),
|
|
135
|
+
upstream.signal,
|
|
136
|
+
);
|
|
137
|
+
const replayThrough = async (through: number) => {
|
|
138
|
+
while (after < through) {
|
|
139
|
+
const previous = after;
|
|
140
|
+
const page = await observe(
|
|
141
|
+
() =>
|
|
142
|
+
client.listEventPage(workspaceId, sessionId, {
|
|
143
|
+
after,
|
|
144
|
+
before: through + 1,
|
|
145
|
+
direction: "after",
|
|
146
|
+
limit: 200,
|
|
147
|
+
mode: "forensic",
|
|
148
|
+
payloadMode: "full",
|
|
149
|
+
}),
|
|
150
|
+
upstream.signal,
|
|
151
|
+
);
|
|
152
|
+
for (const event of page.events) {
|
|
153
|
+
consume(event);
|
|
154
|
+
if (terminal && after > cutoff) return;
|
|
155
|
+
}
|
|
156
|
+
// Byte-limited short pages are not EOF; follow durable coverage.
|
|
157
|
+
after = Math.max(after, page.nextAfter ?? page.coveredSequence?.last ?? 0);
|
|
158
|
+
if (after <= previous)
|
|
159
|
+
throw new Error("The Site AI event replay did not reach the session cursor.");
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const waiting = (session: Session) =>
|
|
163
|
+
session.status === "requires_action" &&
|
|
164
|
+
(!session.activeTurnId || !fold?.turnId || session.activeTurnId === fold.turnId);
|
|
165
|
+
try {
|
|
166
|
+
let session = await readSession();
|
|
167
|
+
// Reconstruct the exact prefix separately: unlike SSE coalescing, this
|
|
168
|
+
// bounded native page read cannot merge output from both sides of cutoff.
|
|
169
|
+
await replayThrough(Math.min(cutoff, session.lastSequence));
|
|
170
|
+
if (terminal) return reply(terminal);
|
|
171
|
+
for (;;) {
|
|
172
|
+
await replayThrough(session.lastSequence);
|
|
173
|
+
if (terminal) return reply(terminal);
|
|
174
|
+
if (waiting(session)) {
|
|
175
|
+
const fresh = await readSession();
|
|
176
|
+
if (fresh.lastSequence > after) {
|
|
177
|
+
session = fresh;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (waiting(fresh)) {
|
|
181
|
+
if (!fold) selectTurn(fresh.activeTurnId ?? lastTurnId(prefix));
|
|
182
|
+
else if (!fold.turnId) fold.turnId = fresh.activeTurnId ?? null;
|
|
183
|
+
if (pending.event && eventCursor(pending.event) > cutoff)
|
|
184
|
+
emit(fold!.push(pending.event).chunks);
|
|
185
|
+
return reply("pending");
|
|
186
|
+
}
|
|
187
|
+
session = fresh;
|
|
188
|
+
}
|
|
189
|
+
streamAbort = new AbortController();
|
|
190
|
+
stream = client.streamEvents(workspaceId, sessionId, {
|
|
191
|
+
after: Math.max(cutoff, after),
|
|
192
|
+
signal: streamAbort.signal,
|
|
193
|
+
});
|
|
194
|
+
for (;;) {
|
|
195
|
+
const next = await observe(() => stream!.next(), upstream.signal);
|
|
196
|
+
if (next.done) throw new Error("The Site AI event stream ended before the turn settled.");
|
|
197
|
+
consume(next.value);
|
|
198
|
+
if (terminal) return reply(terminal);
|
|
199
|
+
if (isWait(next.value) && (!next.value.turnId || next.value.turnId === fold?.turnId)) {
|
|
200
|
+
// Reconcile the finite durable tail before declaring a live wait.
|
|
201
|
+
// Reopen from its cursor so buffered SSE overlap cannot repeat text.
|
|
202
|
+
closeStream();
|
|
203
|
+
session = await readSession();
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
210
|
+
upstream.abort();
|
|
211
|
+
closeStream();
|
|
212
|
+
upstream.signal.removeEventListener("abort", onUpstreamAbort);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
async run(request, options = {}) {
|
|
217
|
+
options.signal?.throwIfAborted();
|
|
218
|
+
// Pick the message fields rather than forwarding a caller's extra runtime
|
|
219
|
+
// properties. Tools, ownership, credentials and limits belong to the host.
|
|
220
|
+
const session = await client.createSession(workspaceId, {
|
|
221
|
+
initialMessage: request.initialMessage,
|
|
222
|
+
idempotencyKey: request.idempotencyKey ?? crypto.randomUUID(),
|
|
223
|
+
...(request.model === undefined ? {} : { model: request.model }),
|
|
224
|
+
...(request.reasoningEffort === undefined
|
|
225
|
+
? {}
|
|
226
|
+
: { reasoningEffort: request.reasoningEffort }),
|
|
227
|
+
...(request.latencyMode === undefined ? {} : { latencyMode: request.latencyMode }),
|
|
228
|
+
});
|
|
229
|
+
options.onSession?.(session.id);
|
|
230
|
+
return await result(session.id, options);
|
|
231
|
+
},
|
|
232
|
+
result,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isWait(event: SessionEvent): boolean {
|
|
237
|
+
return (
|
|
238
|
+
event.type === "session.requiresAction" ||
|
|
239
|
+
event.type === "session.humanInput.requested" ||
|
|
240
|
+
(event.type === "session.status.changed" &&
|
|
241
|
+
asRecord(event.payload).status === "requires_action")
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isTurnOutput(event: SessionEvent): boolean {
|
|
246
|
+
return (
|
|
247
|
+
event.type.startsWith("turn.") ||
|
|
248
|
+
event.type.startsWith("agent.") ||
|
|
249
|
+
event.type === "user.message"
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function lastTurnId(events: SessionEvent[]): string | null {
|
|
254
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
255
|
+
const event = events[index]!;
|
|
256
|
+
if (event.turnId && (isTurnOutput(event) || isWait(event))) return event.turnId;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function eventCursor(event: SessionEvent): number {
|
|
262
|
+
return Math.max(
|
|
263
|
+
event.sequence,
|
|
264
|
+
event.coveredThrough ?? 0,
|
|
265
|
+
sessionEventStreamCoveredThrough(event) ?? 0,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Event-page reads have no signal option; observation can still stop immediately. */
|
|
270
|
+
async function observe<T>(read: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
|
271
|
+
signal.throwIfAborted();
|
|
272
|
+
let onAbort: () => void = () => {};
|
|
273
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
274
|
+
onAbort = () => reject(signal.reason);
|
|
275
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
276
|
+
});
|
|
277
|
+
try {
|
|
278
|
+
const value = await Promise.race([read(), aborted]);
|
|
279
|
+
signal.throwIfAborted();
|
|
280
|
+
return value;
|
|
281
|
+
} finally {
|
|
282
|
+
signal.removeEventListener("abort", onAbort);
|
|
283
|
+
}
|
|
284
|
+
}
|