@eyepop.ai/eyepop 3.25.3 → 3.25.5

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.
@@ -8698,6 +8698,7 @@ __export(index_exports, {
8698
8698
  ModelType: () => ModelType,
8699
8699
  MotionModel: () => MotionModel,
8700
8700
  PIPELINE_PATH: () => PIPELINE_PATH,
8701
+ PIPELINE_SOCKETS: () => PIPELINE_SOCKETS,
8701
8702
  PipelineBackend: () => PipelineBackend,
8702
8703
  PipelineCommandError: () => PipelineCommandError,
8703
8704
  PipelineConfigurationError: () => PipelineConfigurationError,
@@ -9043,14 +9044,25 @@ if ("document" in globalThis && "implementation" in globalThis.document) {
9043
9044
  }
9044
9045
 
9045
9046
  // endpoint.ts
9047
+ var ACCOUNT_CLAIM = "https://ident.eyepop.ai/account-uuid";
9048
+ var M2M_REFUSAL = "API key or user login required";
9049
+ function carriesAccount(token) {
9050
+ try {
9051
+ const payload = (token.split(".")[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
9052
+ return Boolean(JSON.parse(atob(payload))?.[ACCOUNT_CLAIM]);
9053
+ } catch {
9054
+ return false;
9055
+ }
9056
+ }
9046
9057
  var Endpoint = class {
9047
9058
  constructor(options) {
9059
+ this._exchanging = null;
9048
9060
  this._options = options;
9049
9061
  this._token = null;
9050
9062
  this._expire_token_time = null;
9051
9063
  this._client = null;
9052
9064
  this._statusRetryHandlers = /* @__PURE__ */ new Map();
9053
- this._statusRetryHandlers.set(401, this.statusHandler401);
9065
+ this._statusRetryHandlers.set(401, (statusCode, failedToken) => this.statusHandler401(statusCode, failedToken));
9054
9066
  this._limit = new Semaphore(this._options.jobQueueLength ?? 1024);
9055
9067
  this._state = "Idle" /* Idle */;
9056
9068
  this._message = null;
@@ -9066,7 +9078,7 @@ var Endpoint = class {
9066
9078
  }
9067
9079
  setStatusRetryHandlers(statusRetryHandlers = void 0) {
9068
9080
  this._statusRetryHandlers = new Map(statusRetryHandlers);
9069
- this._statusRetryHandlers.set(401, this.statusHandler401);
9081
+ this._statusRetryHandlers.set(401, (statusCode, failedToken) => this.statusHandler401(statusCode, failedToken));
9070
9082
  }
9071
9083
  eyepopUrl() {
9072
9084
  if (this._options.eyepopUrl) {
@@ -9163,12 +9175,15 @@ var Endpoint = class {
9163
9175
  validUntil: this._expire_token_time * 1e3
9164
9176
  };
9165
9177
  }
9166
- statusHandler401(_statusCode) {
9167
- this._token = null;
9168
- this._expire_token_time = null;
9178
+ statusHandler401(_statusCode, failedToken = this._token) {
9179
+ if (this._token === failedToken) {
9180
+ this._token = null;
9181
+ this._expire_token_time = null;
9182
+ }
9169
9183
  }
9170
9184
  async fetchWithRetry(fetcher, retries = 1) {
9171
9185
  while (true) {
9186
+ const sentToken = this._token ?? void 0;
9172
9187
  try {
9173
9188
  const response = await fetcher();
9174
9189
  if (response.ok) {
@@ -9176,7 +9191,7 @@ var Endpoint = class {
9176
9191
  } else {
9177
9192
  const handler = this._statusRetryHandlers.get(response.status);
9178
9193
  if (handler != null) {
9179
- handler(response.status);
9194
+ handler(response.status, sentToken);
9180
9195
  if (retries--) {
9181
9196
  this._requestLogger.info("received %d response, retry", response.status);
9182
9197
  } else {
@@ -9210,54 +9225,74 @@ var Endpoint = class {
9210
9225
  }
9211
9226
  return `Bearer ${await this.currentAccessToken()}`;
9212
9227
  }
9228
+ async exchange(client, credential, accountUuid) {
9229
+ const query = accountUuid ? `?account_uuid=${encodeURIComponent(accountUuid)}` : "";
9230
+ return await client.fetch(`${this.eyepopUrl()}/v1/auth/authenticate${query}`, {
9231
+ method: "POST",
9232
+ headers: { Authorization: `Bearer ${credential}` }
9233
+ });
9234
+ }
9235
+ async storeExchangedToken(response, now) {
9236
+ if (response.status != 200) {
9237
+ const message = await response.text();
9238
+ return Promise.reject(`Unexpected status ${response.status}: ${message}`);
9239
+ }
9240
+ const token = await response.json();
9241
+ this._token = token.access_token;
9242
+ this._expire_token_time = now + token.expires_in - 60;
9243
+ }
9244
+ /**
9245
+ * A token that already carries an account, or that the compute API refuses as an exchanged
9246
+ * token ("API key or user login required"), is used as is. Any other refusal is raised.
9247
+ */
9248
+ async exchangeLoginToken(client, loginToken, validUntil, now) {
9249
+ if (!carriesAccount(loginToken)) {
9250
+ const response = await this.exchange(client, loginToken, this._options.accountId);
9251
+ if (response.status != 401 || !(await response.clone().text()).includes(M2M_REFUSAL)) {
9252
+ await this.storeExchangedToken(response, now);
9253
+ return;
9254
+ }
9255
+ }
9256
+ this._token = loginToken;
9257
+ this._expire_token_time = validUntil;
9258
+ }
9213
9259
  async currentAccessToken() {
9260
+ if (!this._token || this._expire_token_time < Date.now() / 1e3) {
9261
+ this._exchanging ?? (this._exchanging = this.authenticate().finally(() => this._exchanging = null));
9262
+ await this._exchanging;
9263
+ }
9264
+ this._logger.debug("using access token, valid for at least %d seconds", this._expire_token_time - Date.now() / 1e3);
9265
+ return this._token;
9266
+ }
9267
+ async authenticate() {
9214
9268
  const now = Date.now() / 1e3;
9215
- if (!this._token || this._expire_token_time < now) {
9216
- this.updateState("Authenticating" /* Authenticating */);
9217
- try {
9218
- const auth = resolveAuth(this._options);
9219
- if (!this._client) {
9220
- return Promise.reject("endpoint not connected");
9221
- } else if (auth?.isLocal !== void 0) {
9222
- this._token = "implicit";
9223
- this._expire_token_time = Number.MAX_VALUE;
9224
- } else if (auth?.session !== void 0) {
9225
- return Promise.reject("temporary access token expired");
9226
- } else if (auth?.oAuth2 !== void 0 && this._options.eyepopUrl) {
9227
- const session = await authenticateBrowserSession(auth.oAuth2, this._options);
9228
- this._token = session.accessToken;
9229
- this._expire_token_time = session.validUntil / 1e3;
9230
- } else if (accessTokenCredential(auth) !== void 0) {
9231
- this._token = accessTokenCredential(auth);
9232
- this._expire_token_time = Number.MAX_VALUE;
9233
- } else if (this._options.eyepopUrl) {
9234
- const credential = apiKeyCredential(auth);
9235
- if (credential === void 0) {
9236
- return Promise.reject("no valid auth option");
9237
- }
9238
- const headers = { Authorization: `Bearer ${credential}` };
9239
- const post_url = `${this.eyepopUrl()}/v1/auth/authenticate`;
9240
- const response = await this._client.fetch(post_url, {
9241
- method: "POST",
9242
- headers
9243
- });
9244
- if (response.status != 200) {
9245
- const message = await response.text();
9246
- return Promise.reject(`Unexpected status ${response.status}: ${message}`);
9247
- }
9248
- const data = await response.json();
9249
- const token = data;
9250
- this._token = token.access_token;
9251
- this._expire_token_time = now + token.expires_in - 60;
9252
- } else {
9269
+ this.updateState("Authenticating" /* Authenticating */);
9270
+ try {
9271
+ const auth = resolveAuth(this._options);
9272
+ if (!this._client) {
9273
+ return Promise.reject("endpoint not connected");
9274
+ } else if (auth?.isLocal !== void 0) {
9275
+ this._token = "implicit";
9276
+ this._expire_token_time = Number.MAX_VALUE;
9277
+ } else if (auth?.session !== void 0) {
9278
+ return Promise.reject("temporary access token expired");
9279
+ } else if (auth?.oAuth2 !== void 0 && this._options.eyepopUrl) {
9280
+ const login = await authenticateBrowserSession(auth.oAuth2, this._options);
9281
+ await this.exchangeLoginToken(this._client, login.accessToken, login.validUntil / 1e3, now);
9282
+ } else if (accessTokenCredential(auth) !== void 0) {
9283
+ await this.exchangeLoginToken(this._client, accessTokenCredential(auth), Number.MAX_VALUE, now);
9284
+ } else if (this._options.eyepopUrl) {
9285
+ const credential = apiKeyCredential(auth);
9286
+ if (credential === void 0) {
9253
9287
  return Promise.reject("no valid auth option");
9254
9288
  }
9255
- } finally {
9256
- this.updateState();
9289
+ await this.storeExchangedToken(await this.exchange(this._client, credential), now);
9290
+ } else {
9291
+ return Promise.reject("no valid auth option");
9257
9292
  }
9293
+ } finally {
9294
+ this.updateState();
9258
9295
  }
9259
- this._logger.debug("using access token, valid for at least %d seconds", this._expire_token_time - now);
9260
- return this._token;
9261
9296
  }
9262
9297
  };
9263
9298
 
@@ -11612,6 +11647,8 @@ if (isBrowser) {
11612
11647
  var PIPELINE_PATH = "/api/v1/pipeline";
11613
11648
  var SUBPROTOCOL = "eyepop.v1";
11614
11649
  var MAX_OPEN_SOURCES = 256;
11650
+ var PIPELINE_SOCKETS = 4;
11651
+ var EXTRA_SOCKET_RETRY_MS = 10 * 1e3;
11615
11652
  var COMBINED_UPLOAD_MAX_BYTES = 8 * 1024 * 1024;
11616
11653
  var CHUNK = 1;
11617
11654
  var COMBINED = 2;
@@ -11731,6 +11768,8 @@ var PipelineSocket = class {
11731
11768
  // The ids of sources sent on this socket that have neither ended nor been refused, as the
11732
11769
  // pipeline API counts them, cancelled ones included.
11733
11770
  this.held = /* @__PURE__ */ new Set();
11771
+ // The held sources that are live. A live source never ends, so nothing queued behind it plays.
11772
+ this.lives = /* @__PURE__ */ new Set();
11734
11773
  // The held sources the pipeline API took: only these end, so only these make room.
11735
11774
  this.accepted = /* @__PURE__ */ new Set();
11736
11775
  // The ends on this socket so far.
@@ -11739,6 +11778,7 @@ var PipelineSocket = class {
11739
11778
  // the next send waits. Null once none is held.
11740
11779
  this.cap = null;
11741
11780
  this._roomWaiters = [];
11781
+ this.heartbeat = null;
11742
11782
  this.closed = new Promise((resolve) => this._markClosed = resolve);
11743
11783
  }
11744
11784
  /** A source ended or the socket closed: wake the send waiting for room. */
@@ -11772,6 +11812,10 @@ var PipelineSocket = class {
11772
11812
  return !this._closed && this.ws.readyState === 1;
11773
11813
  }
11774
11814
  markClosed() {
11815
+ if (this.heartbeat !== null) {
11816
+ clearInterval(this.heartbeat);
11817
+ this.heartbeat = null;
11818
+ }
11775
11819
  this._closed = true;
11776
11820
  this._markClosed();
11777
11821
  this.freed();
@@ -11798,10 +11842,22 @@ var PipelineJob = class {
11798
11842
  this.openSource = null;
11799
11843
  // Stops the job's source on the pipeline, once it was sent.
11800
11844
  this.stopOnPipeline = null;
11845
+ this._milliComputeUnits = void 0;
11801
11846
  }
11802
11847
  get cancelled() {
11803
11848
  return this._cancelled;
11804
11849
  }
11850
+ /** The source's milli compute units: the latest total of its `usage` messages and its `end`. */
11851
+ get milliComputeUnits() {
11852
+ return this._milliComputeUnits;
11853
+ }
11854
+ /** Keep a usage's or an end's milliComputeUnits: each carries the source's total so far. */
11855
+ noteMilliComputeUnits(message) {
11856
+ const value = message["milliComputeUnits"];
11857
+ if (Number.isInteger(value) && value >= 0) {
11858
+ this._milliComputeUnits = value;
11859
+ }
11860
+ }
11805
11861
  command(id) {
11806
11862
  const command = { type: "setSource", id, ...this.source };
11807
11863
  if (this.params.componentParams !== void 0) {
@@ -11860,14 +11916,14 @@ var PipelineBackend = class {
11860
11916
  constructor(ctx) {
11861
11917
  this.ctx = ctx;
11862
11918
  this._pipelineApiUrl = null;
11863
- this._socket = null;
11919
+ this._sockets = [];
11864
11920
  this._connecting = null;
11865
11921
  // Every job's setSource and every changePop goes out through this chain, in the order submitted.
11866
11922
  this._sendChain = Promise.resolve();
11867
11923
  this._disconnecting = false;
11868
11924
  this._nextId = 0;
11869
11925
  this._warned = /* @__PURE__ */ new Set();
11870
- this._heartbeat = null;
11926
+ this._noExtraSocketUntil = 0;
11871
11927
  // Reconnects at once since a source last played.
11872
11928
  this._reconnects = 0;
11873
11929
  this._currentPop = ctx.options.pop ?? null;
@@ -11881,13 +11937,9 @@ var PipelineBackend = class {
11881
11937
  async disconnect(closeEndpoint) {
11882
11938
  await closeEndpoint();
11883
11939
  this._disconnecting = true;
11884
- if (this._heartbeat !== null) {
11885
- clearInterval(this._heartbeat);
11886
- this._heartbeat = null;
11887
- }
11888
- const socket = this._socket;
11889
- this._socket = null;
11890
- if (socket !== null) {
11940
+ const sockets = this._sockets;
11941
+ this._sockets = [];
11942
+ for (const socket of sockets) {
11891
11943
  socket.ws.close(1e3);
11892
11944
  await socket.closed;
11893
11945
  }
@@ -11905,21 +11957,55 @@ var PipelineBackend = class {
11905
11957
  this._sendChain = result.catch(() => void 0);
11906
11958
  return result;
11907
11959
  }
11908
- /** The open socket, or a new one with the last pop set on it. */
11960
+ openSockets() {
11961
+ return this._sockets.filter((socket) => socket.open);
11962
+ }
11963
+ /** An open socket, or a new one with the last pop set on it. */
11909
11964
  async ensureSocket() {
11910
- if (this._socket !== null && this._socket.open) {
11911
- return this._socket;
11965
+ const open = this.openSockets()[0];
11966
+ if (open !== void 0) {
11967
+ return open;
11912
11968
  }
11913
11969
  if (this._connecting === null) {
11914
- this._connecting = this.openSocket().finally(() => this._connecting = null);
11970
+ this._connecting = this.openSocket(true).finally(() => this._connecting = null);
11915
11971
  }
11916
11972
  return await this._connecting;
11917
11973
  }
11918
- async openSocket() {
11974
+ /**
11975
+ * The socket for the next job; the caller runs it in submission order. A live job goes on a socket
11976
+ * of its own, an idle one or a new one, which `pipelineSockets` does not count. Any other job skips
11977
+ * the sockets with a live source: a new one while every other socket has a source and fewer than
11978
+ * `pipelineSockets` are open, else the one with room and the fewest sources.
11979
+ */
11980
+ async socketForJob(job) {
11981
+ if (job.live) {
11982
+ return this.openSockets().find((socket) => socket.held.size === 0) ?? await this.openSocket(true);
11983
+ }
11984
+ const sockets = this.openSockets().filter((socket) => socket.lives.size === 0);
11985
+ if (sockets.length === 0) {
11986
+ return await this.openSocket(true);
11987
+ }
11988
+ if (sockets.length < (this.ctx.options.pipelineSockets ?? PIPELINE_SOCKETS) && sockets.every((socket) => socket.held.size > 0) && Date.now() >= this._noExtraSocketUntil) {
11989
+ try {
11990
+ return await this.openSocket(false);
11991
+ } catch (error) {
11992
+ this._noExtraSocketUntil = Date.now() + EXTRA_SOCKET_RETRY_MS;
11993
+ this.ctx.requestLogger.info("could not open another pipeline API socket: %s", error);
11994
+ }
11995
+ }
11996
+ const rank = (socket) => (socket.hasRoom ? 0 : MAX_OPEN_SOURCES + 1) + socket.held.size;
11997
+ return sockets.reduce((best, socket) => rank(socket) < rank(best) ? socket : best);
11998
+ }
11999
+ /** A new socket with the last pop set on it; `patient` waits through 503 with Retry-After. */
12000
+ async openSocket(patient) {
11919
12001
  if (this.ctx.client() === null) {
11920
12002
  throw new PipelineSocketClosedError(void 0, "the endpoint is not connected");
11921
12003
  }
11922
- const ws = await this.upgrade(await this.socketUrl());
12004
+ const ws = await this.upgrade(await this.socketUrl(), patient);
12005
+ if (this._disconnecting) {
12006
+ ws.close(1e3);
12007
+ throw new PipelineSocketClosedError(void 0, "the endpoint disconnected");
12008
+ }
11923
12009
  const socket = new PipelineSocket(ws);
11924
12010
  ws.onmessage = (event) => {
11925
12011
  if (typeof event.data === "string") {
@@ -11933,31 +12019,29 @@ var PipelineBackend = class {
11933
12019
  ws.onerror = () => {
11934
12020
  };
11935
12021
  ws.onclose = (event) => this.closed(socket, event.code, event.reason);
11936
- this._socket = socket;
11937
- this.startHeartbeat(ws);
12022
+ this._sockets.push(socket);
12023
+ this.startHeartbeat(socket);
11938
12024
  if (this._currentPop !== null) {
11939
12025
  try {
11940
12026
  await this.setPopOn(socket, this._currentPop);
11941
12027
  } catch (error) {
11942
- this._socket = null;
12028
+ this._sockets = this._sockets.filter((other) => other !== socket);
11943
12029
  ws.close(1e3);
11944
12030
  throw error;
11945
12031
  }
11946
12032
  }
11947
12033
  return socket;
11948
12034
  }
11949
- startHeartbeat(ws) {
11950
- if (this._heartbeat !== null) {
11951
- clearInterval(this._heartbeat);
11952
- }
12035
+ startHeartbeat(socket) {
12036
+ const ws = socket.ws;
11953
12037
  const ping = ws.ping;
11954
12038
  if (typeof ping === "function") {
11955
- this._heartbeat = setInterval(() => {
12039
+ socket.heartbeat = setInterval(() => {
11956
12040
  if (ws.readyState === 1) {
11957
12041
  ping.call(ws);
11958
12042
  }
11959
12043
  }, HEARTBEAT_MS);
11960
- this._heartbeat.unref?.();
12044
+ socket.heartbeat.unref?.();
11961
12045
  }
11962
12046
  }
11963
12047
  async socketUrl() {
@@ -11995,7 +12079,7 @@ var PipelineBackend = class {
11995
12079
  return (await this.ctx.credentialHeader()).replace(/^Bearer /, "");
11996
12080
  }
11997
12081
  /** Open the socket, waiting through 503 with Retry-After and exchanging the API key again after a 401. */
11998
- async upgrade(url) {
12082
+ async upgrade(url, patient) {
11999
12083
  const deadline = Date.now() + MAX_RETRY_MS;
12000
12084
  let rejected = false;
12001
12085
  while (true) {
@@ -12013,7 +12097,7 @@ var PipelineBackend = class {
12013
12097
  continue;
12014
12098
  }
12015
12099
  const wait = error.status === 503 && error.retryAfter !== void 0 ? Math.max(error.retryAfter, 1) * 1e3 : void 0;
12016
- if (wait === void 0 || Date.now() + wait > deadline) {
12100
+ if (!patient || wait === void 0 || Date.now() + wait > deadline) {
12017
12101
  throw error;
12018
12102
  }
12019
12103
  this.ctx.requestLogger.info("the pipeline API answered 503, retrying in %d ms", wait);
@@ -12023,9 +12107,7 @@ var PipelineBackend = class {
12023
12107
  }
12024
12108
  closed(socket, code, reason) {
12025
12109
  socket.markClosed();
12026
- if (this._socket === socket) {
12027
- this._socket = null;
12028
- }
12110
+ this._sockets = this._sockets.filter((other) => other !== socket);
12029
12111
  const error = new PipelineSocketClosedError(code, reason);
12030
12112
  socket.sources.forEach((source) => source.close(error));
12031
12113
  socket.commands.forEach((command) => command.reject(error));
@@ -12051,12 +12133,14 @@ var PipelineBackend = class {
12051
12133
  const source = typeof id === "string" ? socket.sources.get(id) : void 0;
12052
12134
  if (type === "end") {
12053
12135
  socket.held.delete(id);
12136
+ socket.lives.delete(id);
12054
12137
  socket.accepted.delete(id);
12055
12138
  socket.freed();
12056
12139
  } else if (type === "result" && socket.held.has(id)) {
12057
12140
  socket.accepted.add(id);
12058
12141
  } else if (type === "error" && message["error"] !== "unknown_command") {
12059
12142
  socket.held.delete(id);
12143
+ socket.lives.delete(id);
12060
12144
  socket.wake();
12061
12145
  }
12062
12146
  if (type === "send" || type === "credit") {
@@ -12098,6 +12182,10 @@ var PipelineBackend = class {
12098
12182
  command(socket, message) {
12099
12183
  const id = this.newId(message["type"]);
12100
12184
  return new Promise((resolve, reject) => {
12185
+ if (!socket.open) {
12186
+ reject(new PipelineSocketClosedError(void 0, "the socket closed"));
12187
+ return;
12188
+ }
12101
12189
  socket.commands.set(id, { resolve, reject });
12102
12190
  socket.sendJson({ ...message, id });
12103
12191
  });
@@ -12106,15 +12194,28 @@ var PipelineBackend = class {
12106
12194
  pop() {
12107
12195
  return this._currentPop;
12108
12196
  }
12109
- /** Set the pop on the socket's pipeline; a pop it refuses rejects, and the last pop it took stays. */
12197
+ /**
12198
+ * Set the pop on every open socket's pipeline; a pop the first refuses rejects, and the last pop it
12199
+ * took stays. shortcut: a refusal from a later socket, which the first took, leaves the earlier ones
12200
+ * on the new pop; a refusal is the pop's own fault, so every socket answers it alike.
12201
+ */
12110
12202
  async changePop(pop) {
12111
12203
  if (this.ctx.client() === null) {
12112
12204
  this._currentPop = pop;
12113
12205
  return;
12114
12206
  }
12115
12207
  await this.inOrder(async () => {
12116
- if (this._socket !== null && this._socket.open) {
12117
- await this.setPopOn(this._socket, pop);
12208
+ const sockets = this.openSockets();
12209
+ if (sockets.length > 0) {
12210
+ for (const socket of sockets) {
12211
+ try {
12212
+ await this.setPopOn(socket, pop);
12213
+ } catch (error) {
12214
+ if (!(error instanceof PipelineSocketClosedError)) {
12215
+ throw error;
12216
+ }
12217
+ }
12218
+ }
12118
12219
  this._currentPop = pop;
12119
12220
  return;
12120
12221
  }
@@ -12275,22 +12376,30 @@ var PipelineBackend = class {
12275
12376
  }
12276
12377
  /** Send the job's setSource; the caller runs it in submission order. */
12277
12378
  async setSource(job) {
12278
- if (job.cancelled) {
12279
- return null;
12280
- }
12281
- let socket = await this.ensureSocket();
12282
- await socket.room(job);
12283
12379
  if (job.cancelled) {
12284
12380
  return null;
12285
12381
  }
12286
12382
  const combined = await combinedMedia(job);
12287
- socket = await this.ensureSocket();
12383
+ let socket;
12384
+ while (true) {
12385
+ socket = await this.socketForJob(job);
12386
+ await socket.room(job);
12387
+ if (job.cancelled) {
12388
+ return null;
12389
+ }
12390
+ if (socket.open) {
12391
+ break;
12392
+ }
12393
+ }
12288
12394
  const source = new OpenSource(this.newId("source"));
12289
12395
  if (combined === null) {
12290
12396
  uploadKeys(job).forEach((key) => source.uploads.set(key, new Credit()));
12291
12397
  }
12292
12398
  socket.sources.set(source.id, source);
12293
12399
  socket.held.add(source.id);
12400
+ if (job.live) {
12401
+ socket.lives.add(source.id);
12402
+ }
12294
12403
  source.endedBefore = socket.ended;
12295
12404
  job.openSource = source;
12296
12405
  job.stopOnPipeline = () => {
@@ -12326,7 +12435,11 @@ var PipelineBackend = class {
12326
12435
  case "prediction":
12327
12436
  job.predicted(message["prediction"]);
12328
12437
  break;
12438
+ case "usage":
12439
+ job.noteMilliComputeUnits(message);
12440
+ break;
12329
12441
  case "end":
12442
+ job.noteMilliComputeUnits(message);
12330
12443
  if (message["reason"] === "eos") {
12331
12444
  return null;
12332
12445
  }
@@ -12566,6 +12679,9 @@ var WorkerEndpoint = class extends Endpoint {
12566
12679
  if (options.jobQueueLength > MAX_OPEN_SOURCES) {
12567
12680
  throw new PipelineConfigurationError("job_queue_length", `jobQueueLength ${options.jobQueueLength} exceeds ${MAX_OPEN_SOURCES}, the open sources a pipeline API socket takes`);
12568
12681
  }
12682
+ if (options.pipelineSockets !== void 0 && !(Number.isInteger(options.pipelineSockets) && options.pipelineSockets > 0)) {
12683
+ throw new PipelineConfigurationError("pipeline_sockets", `pipelineSockets ${options.pipelineSockets} is not a positive whole number`);
12684
+ }
12569
12685
  }
12570
12686
  super(options);
12571
12687
  const context = {
@@ -12887,6 +13003,7 @@ var InferAssetJob = class {
12887
13003
  constructor(assetUrl, inferRequest, vlmFetch, requestLogger) {
12888
13004
  this._response = null;
12889
13005
  this._runInfo = null;
13006
+ this._milliComputeUnits = void 0;
12890
13007
  this._controller = new AbortController();
12891
13008
  this._vlmFetch = vlmFetch;
12892
13009
  this._assetUrl = assetUrl;
@@ -12923,6 +13040,13 @@ var InferAssetJob = class {
12923
13040
  }
12924
13041
  return iterator();
12925
13042
  }
13043
+ /**
13044
+ * The milli compute units vlm-server billed for the inference, 0 on a cache hit; undefined before it answered
13045
+ * or when it reported none.
13046
+ */
13047
+ get milliComputeUnits() {
13048
+ return this._milliComputeUnits;
13049
+ }
12926
13050
  start(done) {
12927
13051
  this._response = new Promise(async (resolve, reject) => {
12928
13052
  try {
@@ -12947,6 +13071,8 @@ var InferAssetJob = class {
12947
13071
  const runInfo = response["run_info"];
12948
13072
  if (predictions) {
12949
13073
  this._runInfo = runInfo;
13074
+ const used = response["milli_compute_units"] ?? response["compute_units"];
13075
+ this._milliComputeUnits = typeof used === "number" ? used : void 0;
12950
13076
  resolve(predictions);
12951
13077
  break;
12952
13078
  } else {
@@ -14541,6 +14667,7 @@ var index_default = EyePop;
14541
14667
  ModelType,
14542
14668
  MotionModel,
14543
14669
  PIPELINE_PATH,
14670
+ PIPELINE_SOCKETS,
14544
14671
  PipelineBackend,
14545
14672
  PipelineCommandError,
14546
14673
  PipelineConfigurationError,