@eyepop.ai/eyepop 3.25.2 → 3.25.4

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,
@@ -9050,7 +9051,7 @@ var Endpoint = class {
9050
9051
  this._expire_token_time = null;
9051
9052
  this._client = null;
9052
9053
  this._statusRetryHandlers = /* @__PURE__ */ new Map();
9053
- this._statusRetryHandlers.set(401, this.statusHandler401);
9054
+ this._statusRetryHandlers.set(401, (statusCode) => this.statusHandler401(statusCode));
9054
9055
  this._limit = new Semaphore(this._options.jobQueueLength ?? 1024);
9055
9056
  this._state = "Idle" /* Idle */;
9056
9057
  this._message = null;
@@ -9066,7 +9067,7 @@ var Endpoint = class {
9066
9067
  }
9067
9068
  setStatusRetryHandlers(statusRetryHandlers = void 0) {
9068
9069
  this._statusRetryHandlers = new Map(statusRetryHandlers);
9069
- this._statusRetryHandlers.set(401, this.statusHandler401);
9070
+ this._statusRetryHandlers.set(401, (statusCode) => this.statusHandler401(statusCode));
9070
9071
  }
9071
9072
  eyepopUrl() {
9072
9073
  if (this._options.eyepopUrl) {
@@ -9329,7 +9330,7 @@ var PredictionVersion = /* @__PURE__ */ ((PredictionVersion3) => {
9329
9330
  PredictionVersion3[PredictionVersion3["V3"] = 3] = "V3";
9330
9331
  return PredictionVersion3;
9331
9332
  })(PredictionVersion || {});
9332
- var DEFAULT_PREDICTION_VERSION = 2 /* V2 */;
9333
+ var DEFAULT_PREDICTION_VERSION = 3 /* V3 */;
9333
9334
  var VideoMode = /* @__PURE__ */ ((VideoMode2) => {
9334
9335
  VideoMode2["STREAM"] = "stream";
9335
9336
  VideoMode2["BUFFER"] = "buffer";
@@ -11040,14 +11041,6 @@ var _SessionBackend = class _SessionBackend {
11040
11041
  }
11041
11042
  return this.ctx.options.pop || null;
11042
11043
  }
11043
- /**
11044
- * A select forward's results come only at V3. Only a Pop that selects asks
11045
- * for it, so a worker too old to know V3 - which also rejects that Pop - is
11046
- * never sent a version it would not accept.
11047
- */
11048
- predictionVersion() {
11049
- return popSelects(this.pop()) ? 3 /* V3 */ : DEFAULT_PREDICTION_VERSION;
11050
- }
11051
11044
  async changePop(pop) {
11052
11045
  if (this.ctx.options.popId == "transient" /* Transient */) {
11053
11046
  await this.changeTransientPop(pop);
@@ -11148,7 +11141,7 @@ var _SessionBackend = class _SessionBackend {
11148
11141
  return { stream: streamSource.stream, mimeType: streamSource.mimeType };
11149
11142
  })
11150
11143
  );
11151
- return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11144
+ return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11152
11145
  } catch (e) {
11153
11146
  limit.release();
11154
11147
  throw e;
@@ -11167,7 +11160,7 @@ var _SessionBackend = class _SessionBackend {
11167
11160
  stream,
11168
11161
  mimeType: mimeTypes?.[i] ?? null
11169
11162
  }));
11170
- return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11163
+ return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11171
11164
  } catch (e) {
11172
11165
  limit.release();
11173
11166
  throw e;
@@ -11182,7 +11175,7 @@ var _SessionBackend = class _SessionBackend {
11182
11175
  await limit.acquire();
11183
11176
  try {
11184
11177
  this.ctx.updateState();
11185
- return this.start(new LoadFromGroupJob(urls, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11178
+ return this.start(new LoadFromGroupJob(urls, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11186
11179
  } catch (e) {
11187
11180
  limit.release();
11188
11181
  throw e;
@@ -11226,14 +11219,14 @@ var _SessionBackend = class _SessionBackend {
11226
11219
  client,
11227
11220
  this.ctx.requestLogger,
11228
11221
  void 0,
11229
- this.predictionVersion()
11222
+ DEFAULT_PREDICTION_VERSION
11230
11223
  ),
11231
11224
  true
11232
11225
  );
11233
11226
  }
11234
11227
  async uploadStream(source, params) {
11235
11228
  return await this.startConnected(
11236
- async (client) => new UploadJob(source.stream, source.mimeType, source.size, source.videoMode, params, async () => this.session(), client, this.ctx.requestLogger, void 0, this.predictionVersion()),
11229
+ async (client) => new UploadJob(source.stream, source.mimeType, source.size, source.videoMode, params, async () => this.session(), client, this.ctx.requestLogger, void 0, DEFAULT_PREDICTION_VERSION),
11237
11230
  true
11238
11231
  );
11239
11232
  }
@@ -11258,7 +11251,7 @@ var _SessionBackend = class _SessionBackend {
11258
11251
  client,
11259
11252
  this.ctx.requestLogger,
11260
11253
  uploadBodyFactory,
11261
- this.predictionVersion()
11254
+ DEFAULT_PREDICTION_VERSION
11262
11255
  );
11263
11256
  });
11264
11257
  }
@@ -11266,13 +11259,13 @@ var _SessionBackend = class _SessionBackend {
11266
11259
  if (source.url.startsWith("file://")) {
11267
11260
  return await this.uploadPath({ path: source.url.substring("file://".length) }, params);
11268
11261
  }
11269
- return await this.startConnected(async (client) => new LoadFromJob(source.url, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11262
+ return await this.startConnected(async (client) => new LoadFromJob(source.url, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11270
11263
  }
11271
11264
  async loadFromAssetUuid(source, params) {
11272
- return await this.startConnected(async (client) => new LoadFromAssetUuidJob(source.assetUuid, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11265
+ return await this.startConnected(async (client) => new LoadFromAssetUuidJob(source.assetUuid, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11273
11266
  }
11274
11267
  async loadMediaStream(source, params) {
11275
- return await this.startConnected(async (client) => new LoadMediaStreamJob(source.mediaStream, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11268
+ return await this.startConnected(async (client) => new LoadMediaStreamJob(source.mediaStream, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11276
11269
  }
11277
11270
  async reconnect() {
11278
11271
  if (this.ctx.options.popId == "transient" /* Transient */) {
@@ -11620,6 +11613,8 @@ if (isBrowser) {
11620
11613
  var PIPELINE_PATH = "/api/v1/pipeline";
11621
11614
  var SUBPROTOCOL = "eyepop.v1";
11622
11615
  var MAX_OPEN_SOURCES = 256;
11616
+ var PIPELINE_SOCKETS = 4;
11617
+ var EXTRA_SOCKET_RETRY_MS = 10 * 1e3;
11623
11618
  var COMBINED_UPLOAD_MAX_BYTES = 8 * 1024 * 1024;
11624
11619
  var CHUNK = 1;
11625
11620
  var COMBINED = 2;
@@ -11722,6 +11717,8 @@ var OpenSource = class {
11722
11717
  this.id = id;
11723
11718
  this.inbox = new Inbox();
11724
11719
  this.uploads = /* @__PURE__ */ new Map();
11720
+ // The socket's ended count when the setSource went out.
11721
+ this.endedBefore = 0;
11725
11722
  }
11726
11723
  close(error) {
11727
11724
  this.uploads.forEach((upload) => upload.end());
@@ -11734,14 +11731,60 @@ var PipelineSocket = class {
11734
11731
  this.sources = /* @__PURE__ */ new Map();
11735
11732
  this.commands = /* @__PURE__ */ new Map();
11736
11733
  this._closed = false;
11734
+ // The ids of sources sent on this socket that have neither ended nor been refused, as the
11735
+ // pipeline API counts them, cancelled ones included.
11736
+ this.held = /* @__PURE__ */ new Set();
11737
+ // The held sources that are live. A live source never ends, so nothing queued behind it plays.
11738
+ this.lives = /* @__PURE__ */ new Set();
11739
+ // The held sources the pipeline API took: only these end, so only these make room.
11740
+ this.accepted = /* @__PURE__ */ new Set();
11741
+ // The ends on this socket so far.
11742
+ this.ended = 0;
11743
+ // How many sources the socket had taken when it last refused queue_full: while as many are held,
11744
+ // the next send waits. Null once none is held.
11745
+ this.cap = null;
11746
+ this._roomWaiters = [];
11747
+ this.heartbeat = null;
11737
11748
  this.closed = new Promise((resolve) => this._markClosed = resolve);
11738
11749
  }
11750
+ /** A source ended or the socket closed: wake the send waiting for room. */
11751
+ freed() {
11752
+ this.ended += 1;
11753
+ this.wake();
11754
+ }
11755
+ /** A held source ended or was refused: wake the send waiting for room. */
11756
+ wake() {
11757
+ if (this.held.size === 0) {
11758
+ this.cap = null;
11759
+ }
11760
+ const waiters = this._roomWaiters;
11761
+ this._roomWaiters = [];
11762
+ waiters.forEach((wake) => wake());
11763
+ }
11764
+ get hasRoom() {
11765
+ return this.cap === null || this.held.size < this.cap;
11766
+ }
11767
+ /**
11768
+ * Wait, while the socket is full, until a source ends, it closes or the job is cancelled. The
11769
+ * caller sends in order, so one send waits here and each end lets one go, instead of every
11770
+ * refused source sending its media again at once.
11771
+ */
11772
+ async room(job) {
11773
+ while (!this.hasRoom && this.open && !job.cancelled) {
11774
+ await Promise.race([new Promise((resolve) => this._roomWaiters.push(resolve)), job.whenCancelled]);
11775
+ }
11776
+ }
11739
11777
  get open() {
11740
11778
  return !this._closed && this.ws.readyState === 1;
11741
11779
  }
11742
11780
  markClosed() {
11781
+ if (this.heartbeat !== null) {
11782
+ clearInterval(this.heartbeat);
11783
+ this.heartbeat = null;
11784
+ }
11743
11785
  this._closed = true;
11744
11786
  this._markClosed();
11787
+ this.freed();
11745
11788
  }
11746
11789
  sendJson(message) {
11747
11790
  this.ws.send(JSON.stringify(message));
@@ -11760,6 +11803,8 @@ var PipelineJob = class {
11760
11803
  this._results = new Inbox();
11761
11804
  this._consumed = false;
11762
11805
  this._cancelled = false;
11806
+ // Settles when the job is cancelled, so that a send waiting for room ends at once.
11807
+ this.whenCancelled = new Promise((resolve) => this._markCancelled = resolve);
11763
11808
  this.openSource = null;
11764
11809
  // Stops the job's source on the pipeline, once it was sent.
11765
11810
  this.stopOnPipeline = null;
@@ -11799,6 +11844,7 @@ var PipelineJob = class {
11799
11844
  return;
11800
11845
  }
11801
11846
  this._cancelled = true;
11847
+ this._markCancelled();
11802
11848
  this._results.push(null);
11803
11849
  this.openSource?.inbox.push(CANCELLED);
11804
11850
  this.stopOnPipeline?.();
@@ -11824,14 +11870,14 @@ var PipelineBackend = class {
11824
11870
  constructor(ctx) {
11825
11871
  this.ctx = ctx;
11826
11872
  this._pipelineApiUrl = null;
11827
- this._socket = null;
11873
+ this._sockets = [];
11828
11874
  this._connecting = null;
11829
11875
  // Every job's setSource and every changePop goes out through this chain, in the order submitted.
11830
11876
  this._sendChain = Promise.resolve();
11831
11877
  this._disconnecting = false;
11832
11878
  this._nextId = 0;
11833
11879
  this._warned = /* @__PURE__ */ new Set();
11834
- this._heartbeat = null;
11880
+ this._noExtraSocketUntil = 0;
11835
11881
  // Reconnects at once since a source last played.
11836
11882
  this._reconnects = 0;
11837
11883
  this._currentPop = ctx.options.pop ?? null;
@@ -11845,13 +11891,9 @@ var PipelineBackend = class {
11845
11891
  async disconnect(closeEndpoint) {
11846
11892
  await closeEndpoint();
11847
11893
  this._disconnecting = true;
11848
- if (this._heartbeat !== null) {
11849
- clearInterval(this._heartbeat);
11850
- this._heartbeat = null;
11851
- }
11852
- const socket = this._socket;
11853
- this._socket = null;
11854
- if (socket !== null) {
11894
+ const sockets = this._sockets;
11895
+ this._sockets = [];
11896
+ for (const socket of sockets) {
11855
11897
  socket.ws.close(1e3);
11856
11898
  await socket.closed;
11857
11899
  }
@@ -11869,21 +11911,55 @@ var PipelineBackend = class {
11869
11911
  this._sendChain = result.catch(() => void 0);
11870
11912
  return result;
11871
11913
  }
11872
- /** The open socket, or a new one with the last pop set on it. */
11914
+ openSockets() {
11915
+ return this._sockets.filter((socket) => socket.open);
11916
+ }
11917
+ /** An open socket, or a new one with the last pop set on it. */
11873
11918
  async ensureSocket() {
11874
- if (this._socket !== null && this._socket.open) {
11875
- return this._socket;
11919
+ const open = this.openSockets()[0];
11920
+ if (open !== void 0) {
11921
+ return open;
11876
11922
  }
11877
11923
  if (this._connecting === null) {
11878
- this._connecting = this.openSocket().finally(() => this._connecting = null);
11924
+ this._connecting = this.openSocket(true).finally(() => this._connecting = null);
11879
11925
  }
11880
11926
  return await this._connecting;
11881
11927
  }
11882
- async openSocket() {
11928
+ /**
11929
+ * The socket for the next job; the caller runs it in submission order. A live job goes on a socket
11930
+ * of its own, an idle one or a new one, which `pipelineSockets` does not count. Any other job skips
11931
+ * the sockets with a live source: a new one while every other socket has a source and fewer than
11932
+ * `pipelineSockets` are open, else the one with room and the fewest sources.
11933
+ */
11934
+ async socketForJob(job) {
11935
+ if (job.live) {
11936
+ return this.openSockets().find((socket) => socket.held.size === 0) ?? await this.openSocket(true);
11937
+ }
11938
+ const sockets = this.openSockets().filter((socket) => socket.lives.size === 0);
11939
+ if (sockets.length === 0) {
11940
+ return await this.openSocket(true);
11941
+ }
11942
+ if (sockets.length < (this.ctx.options.pipelineSockets ?? PIPELINE_SOCKETS) && sockets.every((socket) => socket.held.size > 0) && Date.now() >= this._noExtraSocketUntil) {
11943
+ try {
11944
+ return await this.openSocket(false);
11945
+ } catch (error) {
11946
+ this._noExtraSocketUntil = Date.now() + EXTRA_SOCKET_RETRY_MS;
11947
+ this.ctx.requestLogger.info("could not open another pipeline API socket: %s", error);
11948
+ }
11949
+ }
11950
+ const rank = (socket) => (socket.hasRoom ? 0 : MAX_OPEN_SOURCES + 1) + socket.held.size;
11951
+ return sockets.reduce((best, socket) => rank(socket) < rank(best) ? socket : best);
11952
+ }
11953
+ /** A new socket with the last pop set on it; `patient` waits through 503 with Retry-After. */
11954
+ async openSocket(patient) {
11883
11955
  if (this.ctx.client() === null) {
11884
11956
  throw new PipelineSocketClosedError(void 0, "the endpoint is not connected");
11885
11957
  }
11886
- const ws = await this.upgrade(await this.socketUrl());
11958
+ const ws = await this.upgrade(await this.socketUrl(), patient);
11959
+ if (this._disconnecting) {
11960
+ ws.close(1e3);
11961
+ throw new PipelineSocketClosedError(void 0, "the endpoint disconnected");
11962
+ }
11887
11963
  const socket = new PipelineSocket(ws);
11888
11964
  ws.onmessage = (event) => {
11889
11965
  if (typeof event.data === "string") {
@@ -11897,31 +11973,29 @@ var PipelineBackend = class {
11897
11973
  ws.onerror = () => {
11898
11974
  };
11899
11975
  ws.onclose = (event) => this.closed(socket, event.code, event.reason);
11900
- this._socket = socket;
11901
- this.startHeartbeat(ws);
11976
+ this._sockets.push(socket);
11977
+ this.startHeartbeat(socket);
11902
11978
  if (this._currentPop !== null) {
11903
11979
  try {
11904
11980
  await this.setPopOn(socket, this._currentPop);
11905
11981
  } catch (error) {
11906
- this._socket = null;
11982
+ this._sockets = this._sockets.filter((other) => other !== socket);
11907
11983
  ws.close(1e3);
11908
11984
  throw error;
11909
11985
  }
11910
11986
  }
11911
11987
  return socket;
11912
11988
  }
11913
- startHeartbeat(ws) {
11914
- if (this._heartbeat !== null) {
11915
- clearInterval(this._heartbeat);
11916
- }
11989
+ startHeartbeat(socket) {
11990
+ const ws = socket.ws;
11917
11991
  const ping = ws.ping;
11918
11992
  if (typeof ping === "function") {
11919
- this._heartbeat = setInterval(() => {
11993
+ socket.heartbeat = setInterval(() => {
11920
11994
  if (ws.readyState === 1) {
11921
11995
  ping.call(ws);
11922
11996
  }
11923
11997
  }, HEARTBEAT_MS);
11924
- this._heartbeat.unref?.();
11998
+ socket.heartbeat.unref?.();
11925
11999
  }
11926
12000
  }
11927
12001
  async socketUrl() {
@@ -11959,7 +12033,7 @@ var PipelineBackend = class {
11959
12033
  return (await this.ctx.credentialHeader()).replace(/^Bearer /, "");
11960
12034
  }
11961
12035
  /** Open the socket, waiting through 503 with Retry-After and exchanging the API key again after a 401. */
11962
- async upgrade(url) {
12036
+ async upgrade(url, patient) {
11963
12037
  const deadline = Date.now() + MAX_RETRY_MS;
11964
12038
  let rejected = false;
11965
12039
  while (true) {
@@ -11977,7 +12051,7 @@ var PipelineBackend = class {
11977
12051
  continue;
11978
12052
  }
11979
12053
  const wait = error.status === 503 && error.retryAfter !== void 0 ? Math.max(error.retryAfter, 1) * 1e3 : void 0;
11980
- if (wait === void 0 || Date.now() + wait > deadline) {
12054
+ if (!patient || wait === void 0 || Date.now() + wait > deadline) {
11981
12055
  throw error;
11982
12056
  }
11983
12057
  this.ctx.requestLogger.info("the pipeline API answered 503, retrying in %d ms", wait);
@@ -11987,9 +12061,7 @@ var PipelineBackend = class {
11987
12061
  }
11988
12062
  closed(socket, code, reason) {
11989
12063
  socket.markClosed();
11990
- if (this._socket === socket) {
11991
- this._socket = null;
11992
- }
12064
+ this._sockets = this._sockets.filter((other) => other !== socket);
11993
12065
  const error = new PipelineSocketClosedError(code, reason);
11994
12066
  socket.sources.forEach((source) => source.close(error));
11995
12067
  socket.commands.forEach((command) => command.reject(error));
@@ -12013,6 +12085,18 @@ var PipelineBackend = class {
12013
12085
  const type = message["type"];
12014
12086
  const id = message["id"];
12015
12087
  const source = typeof id === "string" ? socket.sources.get(id) : void 0;
12088
+ if (type === "end") {
12089
+ socket.held.delete(id);
12090
+ socket.lives.delete(id);
12091
+ socket.accepted.delete(id);
12092
+ socket.freed();
12093
+ } else if (type === "result" && socket.held.has(id)) {
12094
+ socket.accepted.add(id);
12095
+ } else if (type === "error" && message["error"] !== "unknown_command") {
12096
+ socket.held.delete(id);
12097
+ socket.lives.delete(id);
12098
+ socket.wake();
12099
+ }
12016
12100
  if (type === "send" || type === "credit") {
12017
12101
  const upload = source?.uploads.get(message["member"] ?? null);
12018
12102
  upload?.grant(Number(type === "send" ? message["credit"] : message["bytes"]));
@@ -12052,6 +12136,10 @@ var PipelineBackend = class {
12052
12136
  command(socket, message) {
12053
12137
  const id = this.newId(message["type"]);
12054
12138
  return new Promise((resolve, reject) => {
12139
+ if (!socket.open) {
12140
+ reject(new PipelineSocketClosedError(void 0, "the socket closed"));
12141
+ return;
12142
+ }
12055
12143
  socket.commands.set(id, { resolve, reject });
12056
12144
  socket.sendJson({ ...message, id });
12057
12145
  });
@@ -12060,15 +12148,28 @@ var PipelineBackend = class {
12060
12148
  pop() {
12061
12149
  return this._currentPop;
12062
12150
  }
12063
- /** Set the pop on the socket's pipeline; a pop it refuses rejects, and the last pop it took stays. */
12151
+ /**
12152
+ * Set the pop on every open socket's pipeline; a pop the first refuses rejects, and the last pop it
12153
+ * took stays. shortcut: a refusal from a later socket, which the first took, leaves the earlier ones
12154
+ * on the new pop; a refusal is the pop's own fault, so every socket answers it alike.
12155
+ */
12064
12156
  async changePop(pop) {
12065
12157
  if (this.ctx.client() === null) {
12066
12158
  this._currentPop = pop;
12067
12159
  return;
12068
12160
  }
12069
12161
  await this.inOrder(async () => {
12070
- if (this._socket !== null && this._socket.open) {
12071
- await this.setPopOn(this._socket, pop);
12162
+ const sockets = this.openSockets();
12163
+ if (sockets.length > 0) {
12164
+ for (const socket of sockets) {
12165
+ try {
12166
+ await this.setPopOn(socket, pop);
12167
+ } catch (error) {
12168
+ if (!(error instanceof PipelineSocketClosedError)) {
12169
+ throw error;
12170
+ }
12171
+ }
12172
+ }
12072
12173
  this._currentPop = pop;
12073
12174
  return;
12074
12175
  }
@@ -12171,7 +12272,12 @@ var PipelineBackend = class {
12171
12272
  }
12172
12273
  };
12173
12274
  }
12174
- /** Run the job's source to its end; a live source refused overloaded goes once more on a new socket. */
12275
+ /**
12276
+ * Run the job's source to its end. A live source refused overloaded goes once more on a new socket.
12277
+ * A source refused queue_full goes again once another source on the socket ended, so it may go after
12278
+ * jobs submitted later; with none of the endpoint's other sources open there, or with an upload from
12279
+ * the caller's own stream, which cannot be read twice, the refusal stands.
12280
+ */
12175
12281
  async run(job) {
12176
12282
  let retried = false;
12177
12283
  while (true) {
@@ -12182,9 +12288,9 @@ var PipelineBackend = class {
12182
12288
  const { socket, source } = sent;
12183
12289
  const uploading = source.uploads.size > 0 ? this.upload(socket, source, job) : Promise.resolve();
12184
12290
  let whip = null;
12185
- let overloaded;
12291
+ let refused;
12186
12292
  try {
12187
- overloaded = await this.follow(job, source, () => {
12293
+ refused = await this.follow(job, source, () => {
12188
12294
  if (job.mediaStream !== void 0) {
12189
12295
  const fail = (error) => {
12190
12296
  job.stopOnPipeline?.();
@@ -12202,9 +12308,19 @@ var PipelineBackend = class {
12202
12308
  await uploading;
12203
12309
  await whip?.close();
12204
12310
  }
12205
- if (!overloaded) {
12311
+ if (refused === null) {
12206
12312
  return;
12207
12313
  }
12314
+ if (refused === "queue_full") {
12315
+ if (socket.accepted.size === 0 && socket.ended === source.endedBefore) {
12316
+ throw new PipelineCommandError("queue_full", "the socket refused the source with none of its own open");
12317
+ }
12318
+ if (job.media.some((media) => media.once)) {
12319
+ throw new PipelineCommandError("queue_full", "the socket refused an upload from a stream, which cannot be sent again");
12320
+ }
12321
+ socket.cap = Math.min(socket.accepted.size, socket.cap ?? socket.accepted.size) || null;
12322
+ continue;
12323
+ }
12208
12324
  if (retried || !job.live) {
12209
12325
  throw new PipelineCommandError("overloaded", "the pipeline API has no capacity for another live source");
12210
12326
  }
@@ -12218,12 +12334,27 @@ var PipelineBackend = class {
12218
12334
  return null;
12219
12335
  }
12220
12336
  const combined = await combinedMedia(job);
12221
- const socket = await this.ensureSocket();
12337
+ let socket;
12338
+ while (true) {
12339
+ socket = await this.socketForJob(job);
12340
+ await socket.room(job);
12341
+ if (job.cancelled) {
12342
+ return null;
12343
+ }
12344
+ if (socket.open) {
12345
+ break;
12346
+ }
12347
+ }
12222
12348
  const source = new OpenSource(this.newId("source"));
12223
12349
  if (combined === null) {
12224
12350
  uploadKeys(job).forEach((key) => source.uploads.set(key, new Credit()));
12225
12351
  }
12226
12352
  socket.sources.set(source.id, source);
12353
+ socket.held.add(source.id);
12354
+ if (job.live) {
12355
+ socket.lives.add(source.id);
12356
+ }
12357
+ source.endedBefore = socket.ended;
12227
12358
  job.openSource = source;
12228
12359
  job.stopOnPipeline = () => {
12229
12360
  if (socket.open) {
@@ -12238,15 +12369,15 @@ var PipelineBackend = class {
12238
12369
  }
12239
12370
  return { socket, source };
12240
12371
  }
12241
- /** Hand the source's predictions to the job until it ends; true when the pipeline API was overloaded. */
12372
+ /** Hand the source's predictions to the job until it ends; the refusal to retry on, overloaded or queue_full. */
12242
12373
  async follow(job, source, onResult) {
12243
12374
  while (true) {
12244
12375
  if (job.cancelled) {
12245
- return false;
12376
+ return null;
12246
12377
  }
12247
12378
  const message = await source.inbox.next();
12248
12379
  if (message === CANCELLED) {
12249
- return false;
12380
+ return null;
12250
12381
  }
12251
12382
  if (message instanceof Error) {
12252
12383
  throw message;
@@ -12260,12 +12391,12 @@ var PipelineBackend = class {
12260
12391
  break;
12261
12392
  case "end":
12262
12393
  if (message["reason"] === "eos") {
12263
- return false;
12394
+ return null;
12264
12395
  }
12265
12396
  throw new PipelineSourceError(source.id, message["reason"] ?? "", message["message"] ?? "");
12266
12397
  case "error":
12267
- if (message["error"] === "overloaded") {
12268
- return true;
12398
+ if (message["error"] === "overloaded" || message["error"] === "queue_full") {
12399
+ return message["error"];
12269
12400
  }
12270
12401
  throw new PipelineCommandError(message["error"] ?? "", message["message"] ?? "");
12271
12402
  case "dropped":
@@ -12409,7 +12540,8 @@ function knownSize(body) {
12409
12540
  return void 0;
12410
12541
  }
12411
12542
  function bodyMedia(body, contentType, size) {
12412
- return { open: async () => body, contentType, size: size ?? knownSize(body) };
12543
+ const once = !(typeof Blob !== "undefined" && body instanceof Blob) && !(body instanceof ArrayBuffer) && !ArrayBuffer.isView(body);
12544
+ return { open: async () => body, contentType, size: size ?? knownSize(body), once };
12413
12545
  }
12414
12546
  function uploadGroup(media) {
12415
12547
  return { group: media.map((m) => ({ upload: { contentType: m.contentType } })) };
@@ -12497,6 +12629,9 @@ var WorkerEndpoint = class extends Endpoint {
12497
12629
  if (options.jobQueueLength > MAX_OPEN_SOURCES) {
12498
12630
  throw new PipelineConfigurationError("job_queue_length", `jobQueueLength ${options.jobQueueLength} exceeds ${MAX_OPEN_SOURCES}, the open sources a pipeline API socket takes`);
12499
12631
  }
12632
+ if (options.pipelineSockets !== void 0 && !(Number.isInteger(options.pipelineSockets) && options.pipelineSockets > 0)) {
12633
+ throw new PipelineConfigurationError("pipeline_sockets", `pipelineSockets ${options.pipelineSockets} is not a positive whole number`);
12634
+ }
12500
12635
  }
12501
12636
  super(options);
12502
12637
  const context = {
@@ -14472,6 +14607,7 @@ var index_default = EyePop;
14472
14607
  ModelType,
14473
14608
  MotionModel,
14474
14609
  PIPELINE_PATH,
14610
+ PIPELINE_SOCKETS,
14475
14611
  PipelineBackend,
14476
14612
  PipelineCommandError,
14477
14613
  PipelineConfigurationError,