@pymthouse/builder-sdk 0.1.0 → 0.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.
Files changed (77) hide show
  1. package/README.md +66 -0
  2. package/dist/{client-BroVFyIy.d.ts → client-BHfjDvIe.d.ts} +49 -1
  3. package/dist/{client-BhC1YhB1.d.cts → client-CvhJEhjV.d.cts} +49 -1
  4. package/dist/config.cjs +59 -3
  5. package/dist/config.cjs.map +1 -1
  6. package/dist/config.d.cts +8 -1
  7. package/dist/config.d.ts +8 -1
  8. package/dist/config.js +57 -4
  9. package/dist/config.js.map +1 -1
  10. package/dist/device-initiate.cjs +1 -1
  11. package/dist/device-initiate.cjs.map +1 -1
  12. package/dist/device-initiate.js +1 -1
  13. package/dist/device-initiate.js.map +1 -1
  14. package/dist/device.cjs +1 -1
  15. package/dist/device.cjs.map +1 -1
  16. package/dist/device.d.cts +1 -1
  17. package/dist/device.d.ts +1 -1
  18. package/dist/device.js +1 -1
  19. package/dist/device.js.map +1 -1
  20. package/dist/env.cjs +794 -36
  21. package/dist/env.cjs.map +1 -1
  22. package/dist/env.d.cts +2 -2
  23. package/dist/env.d.ts +2 -2
  24. package/dist/env.js +794 -36
  25. package/dist/env.js.map +1 -1
  26. package/dist/gateway/client/index.cjs +492 -0
  27. package/dist/gateway/client/index.cjs.map +1 -0
  28. package/dist/gateway/client/index.d.cts +63 -0
  29. package/dist/gateway/client/index.d.ts +63 -0
  30. package/dist/gateway/client/index.js +489 -0
  31. package/dist/gateway/client/index.js.map +1 -0
  32. package/dist/gateway/index.cjs +16 -0
  33. package/dist/gateway/index.cjs.map +1 -0
  34. package/dist/gateway/index.d.cts +52 -0
  35. package/dist/gateway/index.d.ts +52 -0
  36. package/dist/gateway/index.js +10 -0
  37. package/dist/gateway/index.js.map +1 -0
  38. package/dist/gateway/server/index.cjs +1248 -0
  39. package/dist/gateway/server/index.cjs.map +1 -0
  40. package/dist/gateway/server/index.d.cts +31 -0
  41. package/dist/gateway/server/index.d.ts +31 -0
  42. package/dist/gateway/server/index.js +1233 -0
  43. package/dist/gateway/server/index.js.map +1 -0
  44. package/dist/index.cjs +1075 -186
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +6 -4
  47. package/dist/index.d.ts +6 -4
  48. package/dist/index.js +1042 -163
  49. package/dist/index.js.map +1 -1
  50. package/dist/ingest-B3Yi8Tb1.d.cts +271 -0
  51. package/dist/ingest-DoKJTWU9.d.ts +271 -0
  52. package/dist/plan-pricing.cjs +108 -0
  53. package/dist/plan-pricing.cjs.map +1 -0
  54. package/dist/plan-pricing.d.cts +15 -0
  55. package/dist/plan-pricing.d.ts +15 -0
  56. package/dist/plan-pricing.js +98 -0
  57. package/dist/plan-pricing.js.map +1 -0
  58. package/dist/signer/server.cjs +1366 -0
  59. package/dist/signer/server.cjs.map +1 -0
  60. package/dist/signer/server.d.cts +73 -0
  61. package/dist/signer/server.d.ts +73 -0
  62. package/dist/signer/server.js +1331 -0
  63. package/dist/signer/server.js.map +1 -0
  64. package/dist/tokens.d.cts +1 -1
  65. package/dist/tokens.d.ts +1 -1
  66. package/dist/types-_R1AwEZp.d.cts +343 -0
  67. package/dist/types-_R1AwEZp.d.ts +343 -0
  68. package/dist/verify.cjs +1 -1
  69. package/dist/verify.cjs.map +1 -1
  70. package/dist/verify.d.cts +1 -1
  71. package/dist/verify.d.ts +1 -1
  72. package/dist/verify.js +1 -1
  73. package/dist/verify.js.map +1 -1
  74. package/gateway/proto/lp_rpc.proto +542 -0
  75. package/package.json +42 -1
  76. package/dist/types-rKzVXvMu.d.cts +0 -196
  77. package/dist/types-rKzVXvMu.d.ts +0 -196
@@ -0,0 +1,63 @@
1
+ import { StartGatewaySessionRequest, StartGatewaySessionResponse, GatewaySegmentPublishResult, GatewayLiveSubscribeSegment } from '../index.cjs';
2
+
3
+ type SignerCredentials = {
4
+ type: "bearer";
5
+ accessToken: string;
6
+ } | {
7
+ type: "apiKey";
8
+ apiKey: string;
9
+ facadeUrl: string;
10
+ scope?: string;
11
+ clientId?: string;
12
+ };
13
+ declare function resolveSignerToken(credentials: SignerCredentials, fetchImpl?: typeof fetch): Promise<string>;
14
+
15
+ type BrowserGatewayClientOptions = {
16
+ /** Dashboard (or gateway relay) origin, e.g. https://dashboard.example.com */
17
+ baseUrl: string;
18
+ fetch?: typeof fetch;
19
+ };
20
+ declare class BrowserGatewayClient {
21
+ private readonly options;
22
+ private signerToken;
23
+ private sessionId;
24
+ private publishSeq;
25
+ /** Next trickle GET index for orchestrator output (see TrickleSubscriber). */
26
+ private subscribeSeq;
27
+ private subscribeEmptyPolls;
28
+ private readonly fetchImpl;
29
+ constructor(options: BrowserGatewayClientOptions);
30
+ get baseUrl(): string;
31
+ get signerAccessToken(): string | null;
32
+ get activeSessionId(): string | null;
33
+ get nextSubscribeSeq(): number;
34
+ connect(credentials: SignerCredentials): Promise<void>;
35
+ startSession(request: StartGatewaySessionRequest): Promise<StartGatewaySessionResponse>;
36
+ setSignerToken(accessToken: string): void;
37
+ publishSegment(bytes: ArrayBuffer | Uint8Array, options?: {
38
+ seq?: number;
39
+ contentType?: string;
40
+ }): Promise<GatewaySegmentPublishResult>;
41
+ /**
42
+ * Fetch the next orchestrator output segment (sequential walk from subscribeSeq).
43
+ * Matches livepeer-python-gateway TrickleSubscriber / MediaOutput segment iteration.
44
+ */
45
+ subscribeOutputSegment(): Promise<GatewayLiveSubscribeSegment | null>;
46
+ /**
47
+ * Stream the next orchestrator output segment incrementally.
48
+ * Mirrors Python SegmentReader behavior (consume bytes before segment closes).
49
+ */
50
+ subscribeOutputSegmentStream(onChunk: (chunk: Uint8Array) => void): Promise<Omit<GatewayLiveSubscribeSegment, "data" | "byteCount"> & {
51
+ byteCount: number;
52
+ } | null>;
53
+ private readSubscribeBody;
54
+ /** @deprecated Use subscribeOutputSegment() — live-edge-only polling does not advance output seq. */
55
+ subscribeLiveSegment(): Promise<GatewayLiveSubscribeSegment | null>;
56
+ private advanceSubscribeSeq;
57
+ private handleSubscribeEmpty;
58
+ private applyLatestSeqOnEmptyPoll;
59
+ subscribeSegment(seq?: number): Promise<ArrayBuffer | null>;
60
+ stop(): Promise<void>;
61
+ }
62
+
63
+ export { BrowserGatewayClient, type BrowserGatewayClientOptions, type SignerCredentials, resolveSignerToken };
@@ -0,0 +1,63 @@
1
+ import { StartGatewaySessionRequest, StartGatewaySessionResponse, GatewaySegmentPublishResult, GatewayLiveSubscribeSegment } from '../index.js';
2
+
3
+ type SignerCredentials = {
4
+ type: "bearer";
5
+ accessToken: string;
6
+ } | {
7
+ type: "apiKey";
8
+ apiKey: string;
9
+ facadeUrl: string;
10
+ scope?: string;
11
+ clientId?: string;
12
+ };
13
+ declare function resolveSignerToken(credentials: SignerCredentials, fetchImpl?: typeof fetch): Promise<string>;
14
+
15
+ type BrowserGatewayClientOptions = {
16
+ /** Dashboard (or gateway relay) origin, e.g. https://dashboard.example.com */
17
+ baseUrl: string;
18
+ fetch?: typeof fetch;
19
+ };
20
+ declare class BrowserGatewayClient {
21
+ private readonly options;
22
+ private signerToken;
23
+ private sessionId;
24
+ private publishSeq;
25
+ /** Next trickle GET index for orchestrator output (see TrickleSubscriber). */
26
+ private subscribeSeq;
27
+ private subscribeEmptyPolls;
28
+ private readonly fetchImpl;
29
+ constructor(options: BrowserGatewayClientOptions);
30
+ get baseUrl(): string;
31
+ get signerAccessToken(): string | null;
32
+ get activeSessionId(): string | null;
33
+ get nextSubscribeSeq(): number;
34
+ connect(credentials: SignerCredentials): Promise<void>;
35
+ startSession(request: StartGatewaySessionRequest): Promise<StartGatewaySessionResponse>;
36
+ setSignerToken(accessToken: string): void;
37
+ publishSegment(bytes: ArrayBuffer | Uint8Array, options?: {
38
+ seq?: number;
39
+ contentType?: string;
40
+ }): Promise<GatewaySegmentPublishResult>;
41
+ /**
42
+ * Fetch the next orchestrator output segment (sequential walk from subscribeSeq).
43
+ * Matches livepeer-python-gateway TrickleSubscriber / MediaOutput segment iteration.
44
+ */
45
+ subscribeOutputSegment(): Promise<GatewayLiveSubscribeSegment | null>;
46
+ /**
47
+ * Stream the next orchestrator output segment incrementally.
48
+ * Mirrors Python SegmentReader behavior (consume bytes before segment closes).
49
+ */
50
+ subscribeOutputSegmentStream(onChunk: (chunk: Uint8Array) => void): Promise<Omit<GatewayLiveSubscribeSegment, "data" | "byteCount"> & {
51
+ byteCount: number;
52
+ } | null>;
53
+ private readSubscribeBody;
54
+ /** @deprecated Use subscribeOutputSegment() — live-edge-only polling does not advance output seq. */
55
+ subscribeLiveSegment(): Promise<GatewayLiveSubscribeSegment | null>;
56
+ private advanceSubscribeSeq;
57
+ private handleSubscribeEmpty;
58
+ private applyLatestSeqOnEmptyPoll;
59
+ subscribeSegment(seq?: number): Promise<ArrayBuffer | null>;
60
+ stop(): Promise<void>;
61
+ }
62
+
63
+ export { BrowserGatewayClient, type BrowserGatewayClientOptions, type SignerCredentials, resolveSignerToken };
@@ -0,0 +1,489 @@
1
+ // src/gateway/types.ts
2
+ var TRICKLE_SEQ_LATEST = -1;
3
+ var TRICKLE_SEQ_CURRENT = -2;
4
+
5
+ // src/string-utils.ts
6
+ function stripTrailingSlashes(value) {
7
+ let end = value.length;
8
+ while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
9
+ end--;
10
+ }
11
+ return value.slice(0, end);
12
+ }
13
+
14
+ // src/errors.ts
15
+ var PmtHouseError = class extends Error {
16
+ status;
17
+ code;
18
+ details;
19
+ constructor(message, {
20
+ status = 500,
21
+ code = "pymthouse_error",
22
+ details
23
+ } = {}) {
24
+ super(message);
25
+ this.name = "PmtHouseError";
26
+ this.status = status;
27
+ this.code = code;
28
+ this.details = details;
29
+ }
30
+ };
31
+
32
+ // src/signer/fetch-json.ts
33
+ function oauthFailureDescription(parsed, failureLabel, status) {
34
+ if (typeof parsed.error_description === "string") {
35
+ return parsed.error_description;
36
+ }
37
+ if (typeof parsed.error === "string") {
38
+ return parsed.error;
39
+ }
40
+ return `${failureLabel} (${status})`;
41
+ }
42
+ async function readJsonObjectFromResponse(response, options) {
43
+ const text = await response.text();
44
+ let parsed;
45
+ try {
46
+ parsed = text ? JSON.parse(text) : {};
47
+ } catch {
48
+ throw new PmtHouseError(options.invalidJsonMessage, {
49
+ status: 502,
50
+ code: options.invalidJsonCode,
51
+ details: { status: response.status }
52
+ });
53
+ }
54
+ if (!response.ok) {
55
+ const description = oauthFailureDescription(parsed, options.failureLabel, response.status);
56
+ throw new PmtHouseError(description, {
57
+ status: response.status,
58
+ code: typeof parsed.error === "string" ? parsed.error : options.defaultErrorCode,
59
+ details: parsed
60
+ });
61
+ }
62
+ return parsed;
63
+ }
64
+
65
+ // src/signer/device-exchange.ts
66
+ function extractSignerAccessTokenFromExchangeBody(body) {
67
+ const tokenObj = body.token;
68
+ if (tokenObj !== null && typeof tokenObj === "object" && !Array.isArray(tokenObj)) {
69
+ const nested = tokenObj;
70
+ for (const key of ["accessToken", "access_token"]) {
71
+ const value = nested[key];
72
+ if (typeof value === "string" && value.trim()) {
73
+ return value.trim();
74
+ }
75
+ }
76
+ }
77
+ for (const key of ["accessToken", "access_token"]) {
78
+ const value = body[key];
79
+ if (typeof value === "string" && value.trim()) {
80
+ return value.trim();
81
+ }
82
+ }
83
+ throw new PmtHouseError("Device exchange response missing signer access token", {
84
+ status: 502,
85
+ code: "invalid_exchange_response"
86
+ });
87
+ }
88
+ function normalizeDeviceExchangeResponse(minted, options) {
89
+ const scope = minted.scope.trim() || "sign:job";
90
+ const body = {
91
+ access_token: minted.access_token,
92
+ token_type: "Bearer",
93
+ expires_in: minted.expires_in,
94
+ scope,
95
+ balanceUsdMicros: minted.balanceUsdMicros,
96
+ lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros,
97
+ token: {
98
+ accessToken: minted.access_token,
99
+ access_token: minted.access_token,
100
+ expiresIn: minted.expires_in,
101
+ expires_in: minted.expires_in,
102
+ scope,
103
+ balanceUsdMicros: minted.balanceUsdMicros,
104
+ lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros
105
+ }
106
+ };
107
+ const signerUrl = options?.signerUrl?.trim();
108
+ if (signerUrl) {
109
+ body.signerUrl = signerUrl;
110
+ }
111
+ return body;
112
+ }
113
+
114
+ // src/signer/api-key-exchange.ts
115
+ var EXCHANGE_RESPONSE_ERROR = "invalid_exchange_response";
116
+ async function exchangeApiKeyForSigner(options) {
117
+ const fetchImpl = options.fetch ?? fetch;
118
+ const url = `${stripTrailingSlashes(options.facadeUrl)}/api/pymthouse/keys/exchange`;
119
+ const body = { apiKey: options.apiKey };
120
+ if (options.scope?.trim()) {
121
+ body.scope = options.scope.trim();
122
+ }
123
+ if (options.clientId?.trim()) {
124
+ body.clientId = options.clientId.trim();
125
+ }
126
+ const response = await fetchImpl(url, {
127
+ method: "POST",
128
+ headers: {
129
+ "Content-Type": "application/json",
130
+ Accept: "application/json"
131
+ },
132
+ body: JSON.stringify(body),
133
+ cache: "no-store"
134
+ });
135
+ const parsed = await readJsonObjectFromResponse(response, {
136
+ invalidJsonMessage: "API key exchange returned invalid JSON",
137
+ invalidJsonCode: EXCHANGE_RESPONSE_ERROR,
138
+ failureLabel: "API key exchange failed",
139
+ defaultErrorCode: "api_key_exchange_failed"
140
+ });
141
+ const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);
142
+ const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;
143
+ const signerUrl = typeof signerUrlRaw === "string" && signerUrlRaw.trim() ? signerUrlRaw.trim() : void 0;
144
+ return normalizeDeviceExchangeResponse(
145
+ {
146
+ access_token: accessToken,
147
+ expires_in: typeof parsed.expires_in === "number" && Number.isFinite(parsed.expires_in) ? parsed.expires_in : 3600,
148
+ scope: typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : "sign:job",
149
+ balanceUsdMicros: typeof parsed.balanceUsdMicros === "string" ? parsed.balanceUsdMicros : "0",
150
+ lifetimeGrantedUsdMicros: typeof parsed.lifetimeGrantedUsdMicros === "string" ? parsed.lifetimeGrantedUsdMicros : "0"
151
+ },
152
+ { signerUrl }
153
+ );
154
+ }
155
+
156
+ // src/gateway/client/resolve-signer.ts
157
+ var boundFetch = (input, init) => globalThis.fetch(input, init);
158
+ async function resolveSignerToken(credentials, fetchImpl = boundFetch) {
159
+ if (credentials.type === "bearer") {
160
+ const token2 = credentials.accessToken.trim();
161
+ if (!token2) {
162
+ throw new Error("Signer bearer token is empty");
163
+ }
164
+ return token2;
165
+ }
166
+ const exchanged = await exchangeApiKeyForSigner({
167
+ facadeUrl: credentials.facadeUrl,
168
+ apiKey: credentials.apiKey,
169
+ scope: credentials.scope ?? "sign:job",
170
+ clientId: credentials.clientId,
171
+ fetch: fetchImpl
172
+ });
173
+ const token = exchanged.access_token?.trim() || exchanged.token?.accessToken?.trim() || exchanged.token?.access_token?.trim();
174
+ if (!token) {
175
+ throw new Error("API key exchange did not return a signer access token");
176
+ }
177
+ return token;
178
+ }
179
+
180
+ // src/gateway/client/browser-client.ts
181
+ function jsonErrorMessage(body, status, fallback) {
182
+ if (typeof body.message === "string") {
183
+ return body.message;
184
+ }
185
+ if (typeof body.error === "string") {
186
+ return body.error;
187
+ }
188
+ return `${fallback} (${status})`;
189
+ }
190
+ function resolveSubscribeSegmentSeq(requestedSeq, headerSeq) {
191
+ if (Number.isFinite(headerSeq)) {
192
+ return headerSeq;
193
+ }
194
+ if (requestedSeq >= 0) {
195
+ return requestedSeq;
196
+ }
197
+ return 0;
198
+ }
199
+ function isTrickleLeadingIndex(seq) {
200
+ return seq === TRICKLE_SEQ_LATEST || seq === TRICKLE_SEQ_CURRENT;
201
+ }
202
+ function parseHeaderInt(headers, name) {
203
+ const raw = headers.get(name);
204
+ if (!raw) {
205
+ return Number.NaN;
206
+ }
207
+ const parsed = Number.parseInt(raw, 10);
208
+ return Number.isFinite(parsed) ? parsed : Number.NaN;
209
+ }
210
+ var BrowserGatewayClient = class {
211
+ constructor(options) {
212
+ this.options = options;
213
+ this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
214
+ }
215
+ options;
216
+ signerToken = null;
217
+ sessionId = null;
218
+ publishSeq = -1;
219
+ /** Next trickle GET index for orchestrator output (see TrickleSubscriber). */
220
+ subscribeSeq = TRICKLE_SEQ_CURRENT;
221
+ subscribeEmptyPolls = 0;
222
+ fetchImpl;
223
+ get baseUrl() {
224
+ return stripTrailingSlashes(this.options.baseUrl);
225
+ }
226
+ get signerAccessToken() {
227
+ return this.signerToken;
228
+ }
229
+ get activeSessionId() {
230
+ return this.sessionId;
231
+ }
232
+ get nextSubscribeSeq() {
233
+ return this.subscribeSeq;
234
+ }
235
+ async connect(credentials) {
236
+ this.signerToken = await resolveSignerToken(credentials, this.fetchImpl);
237
+ }
238
+ async startSession(request) {
239
+ if (!this.signerToken) {
240
+ throw new Error("Call connect() with signer credentials before startSession()");
241
+ }
242
+ const response = await this.fetchImpl(`${this.baseUrl}/api/gateway/sessions`, {
243
+ method: "POST",
244
+ headers: {
245
+ Authorization: `Bearer ${this.signerToken}`,
246
+ "Content-Type": "application/json",
247
+ Accept: "application/json"
248
+ },
249
+ body: JSON.stringify(request)
250
+ });
251
+ const body = await response.json().catch(() => ({}));
252
+ if (!response.ok) {
253
+ throw new Error(jsonErrorMessage(body, response.status, "startSession failed"));
254
+ }
255
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
256
+ const manifestId = typeof body.manifestId === "string" ? body.manifestId : "";
257
+ if (!sessionId) {
258
+ throw new Error("startSession response missing sessionId");
259
+ }
260
+ this.sessionId = sessionId;
261
+ const initialPublishSeq = typeof body.publishSeq === "number" && Number.isFinite(body.publishSeq) ? body.publishSeq : 0;
262
+ this.publishSeq = initialPublishSeq - 1;
263
+ const initialSubscribeSeq = typeof body.subscribeSeq === "number" && Number.isFinite(body.subscribeSeq) ? body.subscribeSeq : TRICKLE_SEQ_CURRENT;
264
+ this.subscribeSeq = initialSubscribeSeq;
265
+ this.subscribeEmptyPolls = 0;
266
+ const mimeType = typeof body.mimeType === "string" ? body.mimeType : void 0;
267
+ return {
268
+ sessionId,
269
+ manifestId,
270
+ mimeType,
271
+ publishSeq: initialPublishSeq,
272
+ subscribeSeq: initialSubscribeSeq
273
+ };
274
+ }
275
+ setSignerToken(accessToken) {
276
+ const token = accessToken.trim();
277
+ if (!token) {
278
+ throw new Error("Signer bearer token is empty");
279
+ }
280
+ this.signerToken = token;
281
+ }
282
+ async publishSegment(bytes, options) {
283
+ if (!this.sessionId || !this.signerToken) {
284
+ throw new Error("No active gateway session");
285
+ }
286
+ const seq = options?.seq ?? this.publishSeq + 1;
287
+ const part = bytes instanceof Uint8Array ? Uint8Array.from(bytes) : new Uint8Array(bytes);
288
+ const body = new Blob([part]);
289
+ const response = await this.fetchImpl(
290
+ `${this.baseUrl}/api/gateway/sessions/${encodeURIComponent(this.sessionId)}/publish/${seq}`,
291
+ {
292
+ method: "PUT",
293
+ headers: {
294
+ Authorization: `Bearer ${this.signerToken}`,
295
+ "Content-Type": options?.contentType ?? "application/octet-stream"
296
+ },
297
+ body
298
+ }
299
+ );
300
+ if (!response.ok) {
301
+ const text = await response.text();
302
+ throw new Error(`publishSegment failed (${response.status}): ${text.slice(0, 300)}`);
303
+ }
304
+ this.publishSeq = seq;
305
+ return { seq, ok: true };
306
+ }
307
+ /**
308
+ * Fetch the next orchestrator output segment (sequential walk from subscribeSeq).
309
+ * Matches livepeer-python-gateway TrickleSubscriber / MediaOutput segment iteration.
310
+ */
311
+ async subscribeOutputSegment() {
312
+ const chunks = [];
313
+ const segment = await this.subscribeOutputSegmentStream((chunk) => {
314
+ chunks.push(chunk);
315
+ });
316
+ if (!segment) {
317
+ return null;
318
+ }
319
+ const total = chunks.reduce((sum, part) => sum + part.byteLength, 0);
320
+ const joined = new Uint8Array(total);
321
+ let offset = 0;
322
+ for (const part of chunks) {
323
+ joined.set(part, offset);
324
+ offset += part.byteLength;
325
+ }
326
+ return { ...segment, data: joined.buffer, byteCount: total };
327
+ }
328
+ /**
329
+ * Stream the next orchestrator output segment incrementally.
330
+ * Mirrors Python SegmentReader behavior (consume bytes before segment closes).
331
+ */
332
+ async subscribeOutputSegmentStream(onChunk) {
333
+ if (!this.sessionId || !this.signerToken) {
334
+ throw new Error("No active gateway session");
335
+ }
336
+ const requestedSeq = this.subscribeSeq;
337
+ const url = new URL(
338
+ `${this.baseUrl}/api/gateway/sessions/${encodeURIComponent(this.sessionId)}/subscribe`
339
+ );
340
+ url.searchParams.set("seq", String(requestedSeq));
341
+ const response = await this.fetchImpl(url.toString(), {
342
+ method: "GET",
343
+ headers: {
344
+ Authorization: `Bearer ${this.signerToken}`
345
+ }
346
+ });
347
+ if (response.status === 204) {
348
+ this.handleSubscribeEmpty(requestedSeq, response.headers);
349
+ return null;
350
+ }
351
+ if (!response.ok) {
352
+ const text = await response.text();
353
+ throw new Error(`subscribeOutputSegment failed (${response.status}): ${text.slice(0, 300)}`);
354
+ }
355
+ const segmentSeq = parseHeaderInt(response.headers, "X-Gateway-Segment-Seq");
356
+ const latestSeq = parseHeaderInt(response.headers, "X-Gateway-Latest-Seq");
357
+ const resolvedSegmentSeq = resolveSubscribeSegmentSeq(requestedSeq, segmentSeq);
358
+ const resolvedLatest = Number.isFinite(latestSeq) ? latestSeq : resolvedSegmentSeq;
359
+ const byteCount = await this.readSubscribeBody(response, onChunk);
360
+ if (byteCount === 0) {
361
+ this.handleSubscribeEmpty(requestedSeq, response.headers);
362
+ return null;
363
+ }
364
+ this.advanceSubscribeSeq(requestedSeq, response.headers, resolvedLatest);
365
+ return {
366
+ segmentSeq: resolvedSegmentSeq,
367
+ latestSeq: resolvedLatest,
368
+ nextSeq: this.subscribeSeq,
369
+ byteCount
370
+ };
371
+ }
372
+ async readSubscribeBody(response, onChunk) {
373
+ if (!response.body) {
374
+ const data = await response.arrayBuffer();
375
+ if (data.byteLength === 0) {
376
+ return 0;
377
+ }
378
+ onChunk(new Uint8Array(data));
379
+ return data.byteLength;
380
+ }
381
+ const reader = response.body.getReader();
382
+ let bytesRead = 0;
383
+ try {
384
+ while (true) {
385
+ const { done, value } = await reader.read();
386
+ if (done) {
387
+ break;
388
+ }
389
+ if (value && value.byteLength > 0) {
390
+ bytesRead += value.byteLength;
391
+ onChunk(value);
392
+ }
393
+ }
394
+ } finally {
395
+ reader.releaseLock();
396
+ }
397
+ return bytesRead;
398
+ }
399
+ /** @deprecated Use subscribeOutputSegment() — live-edge-only polling does not advance output seq. */
400
+ async subscribeLiveSegment() {
401
+ return this.subscribeOutputSegment();
402
+ }
403
+ advanceSubscribeSeq(requestedSeq, headers, latestSeq) {
404
+ const nextHeader = parseHeaderInt(headers, "X-Gateway-Next-Seq");
405
+ if (Number.isFinite(nextHeader)) {
406
+ this.subscribeSeq = nextHeader;
407
+ } else {
408
+ const segmentSeq = parseHeaderInt(headers, "X-Gateway-Segment-Seq");
409
+ if (Number.isFinite(segmentSeq) && segmentSeq >= 0) {
410
+ this.subscribeSeq = segmentSeq + 1;
411
+ } else if (requestedSeq >= 0) {
412
+ this.subscribeSeq = requestedSeq + 1;
413
+ } else if (Number.isFinite(latestSeq) && latestSeq >= 0) {
414
+ this.subscribeSeq = latestSeq + 1;
415
+ } else {
416
+ this.subscribeSeq = TRICKLE_SEQ_LATEST;
417
+ }
418
+ }
419
+ if (Number.isFinite(latestSeq) && latestSeq >= 0 && this.subscribeSeq >= 0 && latestSeq - this.subscribeSeq > 2) {
420
+ this.subscribeSeq = latestSeq;
421
+ }
422
+ }
423
+ handleSubscribeEmpty(requestedSeq, headers) {
424
+ this.subscribeEmptyPolls += 1;
425
+ const latest = parseHeaderInt(headers, "X-Gateway-Latest-Seq");
426
+ if (Number.isFinite(latest) && this.applyLatestSeqOnEmptyPoll(requestedSeq, latest, headers)) {
427
+ return;
428
+ }
429
+ if (requestedSeq >= 0) {
430
+ this.subscribeSeq = requestedSeq;
431
+ return;
432
+ }
433
+ if (this.subscribeEmptyPolls >= 12) {
434
+ this.subscribeSeq = TRICKLE_SEQ_LATEST;
435
+ this.subscribeEmptyPolls = 0;
436
+ }
437
+ }
438
+ applyLatestSeqOnEmptyPoll(requestedSeq, latest, headers) {
439
+ const wait = headers.get("X-Gateway-Wait") === "1";
440
+ if (wait && requestedSeq >= 0 && latest < requestedSeq) {
441
+ this.subscribeSeq = requestedSeq;
442
+ return true;
443
+ }
444
+ if (isTrickleLeadingIndex(requestedSeq)) {
445
+ this.subscribeSeq = latest >= 0 ? latest : TRICKLE_SEQ_LATEST;
446
+ this.subscribeEmptyPolls = 0;
447
+ return true;
448
+ }
449
+ if (requestedSeq >= 0 && latest === requestedSeq) {
450
+ this.subscribeSeq = requestedSeq;
451
+ return true;
452
+ }
453
+ if (requestedSeq >= 0 && latest > requestedSeq) {
454
+ this.subscribeSeq = latest;
455
+ this.subscribeEmptyPolls = 0;
456
+ return true;
457
+ }
458
+ return false;
459
+ }
460
+ async subscribeSegment(seq) {
461
+ if (seq !== void 0) {
462
+ this.subscribeSeq = seq;
463
+ }
464
+ const segment = await this.subscribeOutputSegment();
465
+ return segment?.data ?? null;
466
+ }
467
+ async stop() {
468
+ if (!this.sessionId || !this.signerToken) {
469
+ return;
470
+ }
471
+ await this.fetchImpl(
472
+ `${this.baseUrl}/api/gateway/sessions/${encodeURIComponent(this.sessionId)}`,
473
+ {
474
+ method: "DELETE",
475
+ headers: {
476
+ Authorization: `Bearer ${this.signerToken}`
477
+ }
478
+ }
479
+ ).catch(() => void 0);
480
+ this.sessionId = null;
481
+ this.publishSeq = -1;
482
+ this.subscribeSeq = TRICKLE_SEQ_CURRENT;
483
+ this.subscribeEmptyPolls = 0;
484
+ }
485
+ };
486
+
487
+ export { BrowserGatewayClient, resolveSignerToken };
488
+ //# sourceMappingURL=index.js.map
489
+ //# sourceMappingURL=index.js.map