@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.
@@ -8935,7 +8935,7 @@ var Endpoint = class {
8935
8935
  this._expire_token_time = null;
8936
8936
  this._client = null;
8937
8937
  this._statusRetryHandlers = /* @__PURE__ */ new Map();
8938
- this._statusRetryHandlers.set(401, this.statusHandler401);
8938
+ this._statusRetryHandlers.set(401, (statusCode) => this.statusHandler401(statusCode));
8939
8939
  this._limit = new Semaphore(this._options.jobQueueLength ?? 1024);
8940
8940
  this._state = "Idle" /* Idle */;
8941
8941
  this._message = null;
@@ -8951,7 +8951,7 @@ var Endpoint = class {
8951
8951
  }
8952
8952
  setStatusRetryHandlers(statusRetryHandlers = void 0) {
8953
8953
  this._statusRetryHandlers = new Map(statusRetryHandlers);
8954
- this._statusRetryHandlers.set(401, this.statusHandler401);
8954
+ this._statusRetryHandlers.set(401, (statusCode) => this.statusHandler401(statusCode));
8955
8955
  }
8956
8956
  eyepopUrl() {
8957
8957
  if (this._options.eyepopUrl) {
@@ -9214,7 +9214,7 @@ var PredictionVersion = /* @__PURE__ */ ((PredictionVersion3) => {
9214
9214
  PredictionVersion3[PredictionVersion3["V3"] = 3] = "V3";
9215
9215
  return PredictionVersion3;
9216
9216
  })(PredictionVersion || {});
9217
- var DEFAULT_PREDICTION_VERSION = 2 /* V2 */;
9217
+ var DEFAULT_PREDICTION_VERSION = 3 /* V3 */;
9218
9218
  var VideoMode = /* @__PURE__ */ ((VideoMode2) => {
9219
9219
  VideoMode2["STREAM"] = "stream";
9220
9220
  VideoMode2["BUFFER"] = "buffer";
@@ -10925,14 +10925,6 @@ var _SessionBackend = class _SessionBackend {
10925
10925
  }
10926
10926
  return this.ctx.options.pop || null;
10927
10927
  }
10928
- /**
10929
- * A select forward's results come only at V3. Only a Pop that selects asks
10930
- * for it, so a worker too old to know V3 - which also rejects that Pop - is
10931
- * never sent a version it would not accept.
10932
- */
10933
- predictionVersion() {
10934
- return popSelects(this.pop()) ? 3 /* V3 */ : DEFAULT_PREDICTION_VERSION;
10935
- }
10936
10928
  async changePop(pop) {
10937
10929
  if (this.ctx.options.popId == "transient" /* Transient */) {
10938
10930
  await this.changeTransientPop(pop);
@@ -11033,7 +11025,7 @@ var _SessionBackend = class _SessionBackend {
11033
11025
  return { stream: streamSource.stream, mimeType: streamSource.mimeType };
11034
11026
  })
11035
11027
  );
11036
- return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11028
+ return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11037
11029
  } catch (e) {
11038
11030
  limit.release();
11039
11031
  throw e;
@@ -11052,7 +11044,7 @@ var _SessionBackend = class _SessionBackend {
11052
11044
  stream,
11053
11045
  mimeType: mimeTypes?.[i] ?? null
11054
11046
  }));
11055
- return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11047
+ return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11056
11048
  } catch (e) {
11057
11049
  limit.release();
11058
11050
  throw e;
@@ -11067,7 +11059,7 @@ var _SessionBackend = class _SessionBackend {
11067
11059
  await limit.acquire();
11068
11060
  try {
11069
11061
  this.ctx.updateState();
11070
- return this.start(new LoadFromGroupJob(urls, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11062
+ return this.start(new LoadFromGroupJob(urls, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11071
11063
  } catch (e) {
11072
11064
  limit.release();
11073
11065
  throw e;
@@ -11111,14 +11103,14 @@ var _SessionBackend = class _SessionBackend {
11111
11103
  client,
11112
11104
  this.ctx.requestLogger,
11113
11105
  void 0,
11114
- this.predictionVersion()
11106
+ DEFAULT_PREDICTION_VERSION
11115
11107
  ),
11116
11108
  true
11117
11109
  );
11118
11110
  }
11119
11111
  async uploadStream(source, params) {
11120
11112
  return await this.startConnected(
11121
- async (client) => new UploadJob(source.stream, source.mimeType, source.size, source.videoMode, params, async () => this.session(), client, this.ctx.requestLogger, void 0, this.predictionVersion()),
11113
+ 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),
11122
11114
  true
11123
11115
  );
11124
11116
  }
@@ -11143,7 +11135,7 @@ var _SessionBackend = class _SessionBackend {
11143
11135
  client,
11144
11136
  this.ctx.requestLogger,
11145
11137
  uploadBodyFactory,
11146
- this.predictionVersion()
11138
+ DEFAULT_PREDICTION_VERSION
11147
11139
  );
11148
11140
  });
11149
11141
  }
@@ -11151,13 +11143,13 @@ var _SessionBackend = class _SessionBackend {
11151
11143
  if (source.url.startsWith("file://")) {
11152
11144
  return await this.uploadPath({ path: source.url.substring("file://".length) }, params);
11153
11145
  }
11154
- return await this.startConnected(async (client) => new LoadFromJob(source.url, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11146
+ return await this.startConnected(async (client) => new LoadFromJob(source.url, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11155
11147
  }
11156
11148
  async loadFromAssetUuid(source, params) {
11157
- return await this.startConnected(async (client) => new LoadFromAssetUuidJob(source.assetUuid, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11149
+ return await this.startConnected(async (client) => new LoadFromAssetUuidJob(source.assetUuid, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11158
11150
  }
11159
11151
  async loadMediaStream(source, params) {
11160
- return await this.startConnected(async (client) => new LoadMediaStreamJob(source.mediaStream, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11152
+ return await this.startConnected(async (client) => new LoadMediaStreamJob(source.mediaStream, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11161
11153
  }
11162
11154
  async reconnect() {
11163
11155
  if (this.ctx.options.popId == "transient" /* Transient */) {
@@ -11505,6 +11497,8 @@ if (isBrowser) {
11505
11497
  var PIPELINE_PATH = "/api/v1/pipeline";
11506
11498
  var SUBPROTOCOL = "eyepop.v1";
11507
11499
  var MAX_OPEN_SOURCES = 256;
11500
+ var PIPELINE_SOCKETS = 4;
11501
+ var EXTRA_SOCKET_RETRY_MS = 10 * 1e3;
11508
11502
  var COMBINED_UPLOAD_MAX_BYTES = 8 * 1024 * 1024;
11509
11503
  var CHUNK = 1;
11510
11504
  var COMBINED = 2;
@@ -11607,6 +11601,8 @@ var OpenSource = class {
11607
11601
  this.id = id;
11608
11602
  this.inbox = new Inbox();
11609
11603
  this.uploads = /* @__PURE__ */ new Map();
11604
+ // The socket's ended count when the setSource went out.
11605
+ this.endedBefore = 0;
11610
11606
  }
11611
11607
  close(error) {
11612
11608
  this.uploads.forEach((upload) => upload.end());
@@ -11619,14 +11615,60 @@ var PipelineSocket = class {
11619
11615
  this.sources = /* @__PURE__ */ new Map();
11620
11616
  this.commands = /* @__PURE__ */ new Map();
11621
11617
  this._closed = false;
11618
+ // The ids of sources sent on this socket that have neither ended nor been refused, as the
11619
+ // pipeline API counts them, cancelled ones included.
11620
+ this.held = /* @__PURE__ */ new Set();
11621
+ // The held sources that are live. A live source never ends, so nothing queued behind it plays.
11622
+ this.lives = /* @__PURE__ */ new Set();
11623
+ // The held sources the pipeline API took: only these end, so only these make room.
11624
+ this.accepted = /* @__PURE__ */ new Set();
11625
+ // The ends on this socket so far.
11626
+ this.ended = 0;
11627
+ // How many sources the socket had taken when it last refused queue_full: while as many are held,
11628
+ // the next send waits. Null once none is held.
11629
+ this.cap = null;
11630
+ this._roomWaiters = [];
11631
+ this.heartbeat = null;
11622
11632
  this.closed = new Promise((resolve) => this._markClosed = resolve);
11623
11633
  }
11634
+ /** A source ended or the socket closed: wake the send waiting for room. */
11635
+ freed() {
11636
+ this.ended += 1;
11637
+ this.wake();
11638
+ }
11639
+ /** A held source ended or was refused: wake the send waiting for room. */
11640
+ wake() {
11641
+ if (this.held.size === 0) {
11642
+ this.cap = null;
11643
+ }
11644
+ const waiters = this._roomWaiters;
11645
+ this._roomWaiters = [];
11646
+ waiters.forEach((wake) => wake());
11647
+ }
11648
+ get hasRoom() {
11649
+ return this.cap === null || this.held.size < this.cap;
11650
+ }
11651
+ /**
11652
+ * Wait, while the socket is full, until a source ends, it closes or the job is cancelled. The
11653
+ * caller sends in order, so one send waits here and each end lets one go, instead of every
11654
+ * refused source sending its media again at once.
11655
+ */
11656
+ async room(job) {
11657
+ while (!this.hasRoom && this.open && !job.cancelled) {
11658
+ await Promise.race([new Promise((resolve) => this._roomWaiters.push(resolve)), job.whenCancelled]);
11659
+ }
11660
+ }
11624
11661
  get open() {
11625
11662
  return !this._closed && this.ws.readyState === 1;
11626
11663
  }
11627
11664
  markClosed() {
11665
+ if (this.heartbeat !== null) {
11666
+ clearInterval(this.heartbeat);
11667
+ this.heartbeat = null;
11668
+ }
11628
11669
  this._closed = true;
11629
11670
  this._markClosed();
11671
+ this.freed();
11630
11672
  }
11631
11673
  sendJson(message) {
11632
11674
  this.ws.send(JSON.stringify(message));
@@ -11645,6 +11687,8 @@ var PipelineJob = class {
11645
11687
  this._results = new Inbox();
11646
11688
  this._consumed = false;
11647
11689
  this._cancelled = false;
11690
+ // Settles when the job is cancelled, so that a send waiting for room ends at once.
11691
+ this.whenCancelled = new Promise((resolve) => this._markCancelled = resolve);
11648
11692
  this.openSource = null;
11649
11693
  // Stops the job's source on the pipeline, once it was sent.
11650
11694
  this.stopOnPipeline = null;
@@ -11684,6 +11728,7 @@ var PipelineJob = class {
11684
11728
  return;
11685
11729
  }
11686
11730
  this._cancelled = true;
11731
+ this._markCancelled();
11687
11732
  this._results.push(null);
11688
11733
  this.openSource?.inbox.push(CANCELLED);
11689
11734
  this.stopOnPipeline?.();
@@ -11709,14 +11754,14 @@ var PipelineBackend = class {
11709
11754
  constructor(ctx) {
11710
11755
  this.ctx = ctx;
11711
11756
  this._pipelineApiUrl = null;
11712
- this._socket = null;
11757
+ this._sockets = [];
11713
11758
  this._connecting = null;
11714
11759
  // Every job's setSource and every changePop goes out through this chain, in the order submitted.
11715
11760
  this._sendChain = Promise.resolve();
11716
11761
  this._disconnecting = false;
11717
11762
  this._nextId = 0;
11718
11763
  this._warned = /* @__PURE__ */ new Set();
11719
- this._heartbeat = null;
11764
+ this._noExtraSocketUntil = 0;
11720
11765
  // Reconnects at once since a source last played.
11721
11766
  this._reconnects = 0;
11722
11767
  this._currentPop = ctx.options.pop ?? null;
@@ -11730,13 +11775,9 @@ var PipelineBackend = class {
11730
11775
  async disconnect(closeEndpoint) {
11731
11776
  await closeEndpoint();
11732
11777
  this._disconnecting = true;
11733
- if (this._heartbeat !== null) {
11734
- clearInterval(this._heartbeat);
11735
- this._heartbeat = null;
11736
- }
11737
- const socket = this._socket;
11738
- this._socket = null;
11739
- if (socket !== null) {
11778
+ const sockets = this._sockets;
11779
+ this._sockets = [];
11780
+ for (const socket of sockets) {
11740
11781
  socket.ws.close(1e3);
11741
11782
  await socket.closed;
11742
11783
  }
@@ -11754,21 +11795,55 @@ var PipelineBackend = class {
11754
11795
  this._sendChain = result.catch(() => void 0);
11755
11796
  return result;
11756
11797
  }
11757
- /** The open socket, or a new one with the last pop set on it. */
11798
+ openSockets() {
11799
+ return this._sockets.filter((socket) => socket.open);
11800
+ }
11801
+ /** An open socket, or a new one with the last pop set on it. */
11758
11802
  async ensureSocket() {
11759
- if (this._socket !== null && this._socket.open) {
11760
- return this._socket;
11803
+ const open = this.openSockets()[0];
11804
+ if (open !== void 0) {
11805
+ return open;
11761
11806
  }
11762
11807
  if (this._connecting === null) {
11763
- this._connecting = this.openSocket().finally(() => this._connecting = null);
11808
+ this._connecting = this.openSocket(true).finally(() => this._connecting = null);
11764
11809
  }
11765
11810
  return await this._connecting;
11766
11811
  }
11767
- async openSocket() {
11812
+ /**
11813
+ * The socket for the next job; the caller runs it in submission order. A live job goes on a socket
11814
+ * of its own, an idle one or a new one, which `pipelineSockets` does not count. Any other job skips
11815
+ * the sockets with a live source: a new one while every other socket has a source and fewer than
11816
+ * `pipelineSockets` are open, else the one with room and the fewest sources.
11817
+ */
11818
+ async socketForJob(job) {
11819
+ if (job.live) {
11820
+ return this.openSockets().find((socket) => socket.held.size === 0) ?? await this.openSocket(true);
11821
+ }
11822
+ const sockets = this.openSockets().filter((socket) => socket.lives.size === 0);
11823
+ if (sockets.length === 0) {
11824
+ return await this.openSocket(true);
11825
+ }
11826
+ if (sockets.length < (this.ctx.options.pipelineSockets ?? PIPELINE_SOCKETS) && sockets.every((socket) => socket.held.size > 0) && Date.now() >= this._noExtraSocketUntil) {
11827
+ try {
11828
+ return await this.openSocket(false);
11829
+ } catch (error) {
11830
+ this._noExtraSocketUntil = Date.now() + EXTRA_SOCKET_RETRY_MS;
11831
+ this.ctx.requestLogger.info("could not open another pipeline API socket: %s", error);
11832
+ }
11833
+ }
11834
+ const rank = (socket) => (socket.hasRoom ? 0 : MAX_OPEN_SOURCES + 1) + socket.held.size;
11835
+ return sockets.reduce((best, socket) => rank(socket) < rank(best) ? socket : best);
11836
+ }
11837
+ /** A new socket with the last pop set on it; `patient` waits through 503 with Retry-After. */
11838
+ async openSocket(patient) {
11768
11839
  if (this.ctx.client() === null) {
11769
11840
  throw new PipelineSocketClosedError(void 0, "the endpoint is not connected");
11770
11841
  }
11771
- const ws = await this.upgrade(await this.socketUrl());
11842
+ const ws = await this.upgrade(await this.socketUrl(), patient);
11843
+ if (this._disconnecting) {
11844
+ ws.close(1e3);
11845
+ throw new PipelineSocketClosedError(void 0, "the endpoint disconnected");
11846
+ }
11772
11847
  const socket = new PipelineSocket(ws);
11773
11848
  ws.onmessage = (event) => {
11774
11849
  if (typeof event.data === "string") {
@@ -11782,31 +11857,29 @@ var PipelineBackend = class {
11782
11857
  ws.onerror = () => {
11783
11858
  };
11784
11859
  ws.onclose = (event) => this.closed(socket, event.code, event.reason);
11785
- this._socket = socket;
11786
- this.startHeartbeat(ws);
11860
+ this._sockets.push(socket);
11861
+ this.startHeartbeat(socket);
11787
11862
  if (this._currentPop !== null) {
11788
11863
  try {
11789
11864
  await this.setPopOn(socket, this._currentPop);
11790
11865
  } catch (error) {
11791
- this._socket = null;
11866
+ this._sockets = this._sockets.filter((other) => other !== socket);
11792
11867
  ws.close(1e3);
11793
11868
  throw error;
11794
11869
  }
11795
11870
  }
11796
11871
  return socket;
11797
11872
  }
11798
- startHeartbeat(ws) {
11799
- if (this._heartbeat !== null) {
11800
- clearInterval(this._heartbeat);
11801
- }
11873
+ startHeartbeat(socket) {
11874
+ const ws = socket.ws;
11802
11875
  const ping = ws.ping;
11803
11876
  if (typeof ping === "function") {
11804
- this._heartbeat = setInterval(() => {
11877
+ socket.heartbeat = setInterval(() => {
11805
11878
  if (ws.readyState === 1) {
11806
11879
  ping.call(ws);
11807
11880
  }
11808
11881
  }, HEARTBEAT_MS);
11809
- this._heartbeat.unref?.();
11882
+ socket.heartbeat.unref?.();
11810
11883
  }
11811
11884
  }
11812
11885
  async socketUrl() {
@@ -11844,7 +11917,7 @@ var PipelineBackend = class {
11844
11917
  return (await this.ctx.credentialHeader()).replace(/^Bearer /, "");
11845
11918
  }
11846
11919
  /** Open the socket, waiting through 503 with Retry-After and exchanging the API key again after a 401. */
11847
- async upgrade(url) {
11920
+ async upgrade(url, patient) {
11848
11921
  const deadline = Date.now() + MAX_RETRY_MS;
11849
11922
  let rejected = false;
11850
11923
  while (true) {
@@ -11862,7 +11935,7 @@ var PipelineBackend = class {
11862
11935
  continue;
11863
11936
  }
11864
11937
  const wait = error.status === 503 && error.retryAfter !== void 0 ? Math.max(error.retryAfter, 1) * 1e3 : void 0;
11865
- if (wait === void 0 || Date.now() + wait > deadline) {
11938
+ if (!patient || wait === void 0 || Date.now() + wait > deadline) {
11866
11939
  throw error;
11867
11940
  }
11868
11941
  this.ctx.requestLogger.info("the pipeline API answered 503, retrying in %d ms", wait);
@@ -11872,9 +11945,7 @@ var PipelineBackend = class {
11872
11945
  }
11873
11946
  closed(socket, code, reason) {
11874
11947
  socket.markClosed();
11875
- if (this._socket === socket) {
11876
- this._socket = null;
11877
- }
11948
+ this._sockets = this._sockets.filter((other) => other !== socket);
11878
11949
  const error = new PipelineSocketClosedError(code, reason);
11879
11950
  socket.sources.forEach((source) => source.close(error));
11880
11951
  socket.commands.forEach((command) => command.reject(error));
@@ -11898,6 +11969,18 @@ var PipelineBackend = class {
11898
11969
  const type = message["type"];
11899
11970
  const id = message["id"];
11900
11971
  const source = typeof id === "string" ? socket.sources.get(id) : void 0;
11972
+ if (type === "end") {
11973
+ socket.held.delete(id);
11974
+ socket.lives.delete(id);
11975
+ socket.accepted.delete(id);
11976
+ socket.freed();
11977
+ } else if (type === "result" && socket.held.has(id)) {
11978
+ socket.accepted.add(id);
11979
+ } else if (type === "error" && message["error"] !== "unknown_command") {
11980
+ socket.held.delete(id);
11981
+ socket.lives.delete(id);
11982
+ socket.wake();
11983
+ }
11901
11984
  if (type === "send" || type === "credit") {
11902
11985
  const upload = source?.uploads.get(message["member"] ?? null);
11903
11986
  upload?.grant(Number(type === "send" ? message["credit"] : message["bytes"]));
@@ -11937,6 +12020,10 @@ var PipelineBackend = class {
11937
12020
  command(socket, message) {
11938
12021
  const id = this.newId(message["type"]);
11939
12022
  return new Promise((resolve, reject) => {
12023
+ if (!socket.open) {
12024
+ reject(new PipelineSocketClosedError(void 0, "the socket closed"));
12025
+ return;
12026
+ }
11940
12027
  socket.commands.set(id, { resolve, reject });
11941
12028
  socket.sendJson({ ...message, id });
11942
12029
  });
@@ -11945,15 +12032,28 @@ var PipelineBackend = class {
11945
12032
  pop() {
11946
12033
  return this._currentPop;
11947
12034
  }
11948
- /** Set the pop on the socket's pipeline; a pop it refuses rejects, and the last pop it took stays. */
12035
+ /**
12036
+ * Set the pop on every open socket's pipeline; a pop the first refuses rejects, and the last pop it
12037
+ * took stays. shortcut: a refusal from a later socket, which the first took, leaves the earlier ones
12038
+ * on the new pop; a refusal is the pop's own fault, so every socket answers it alike.
12039
+ */
11949
12040
  async changePop(pop) {
11950
12041
  if (this.ctx.client() === null) {
11951
12042
  this._currentPop = pop;
11952
12043
  return;
11953
12044
  }
11954
12045
  await this.inOrder(async () => {
11955
- if (this._socket !== null && this._socket.open) {
11956
- await this.setPopOn(this._socket, pop);
12046
+ const sockets = this.openSockets();
12047
+ if (sockets.length > 0) {
12048
+ for (const socket of sockets) {
12049
+ try {
12050
+ await this.setPopOn(socket, pop);
12051
+ } catch (error) {
12052
+ if (!(error instanceof PipelineSocketClosedError)) {
12053
+ throw error;
12054
+ }
12055
+ }
12056
+ }
11957
12057
  this._currentPop = pop;
11958
12058
  return;
11959
12059
  }
@@ -12056,7 +12156,12 @@ var PipelineBackend = class {
12056
12156
  }
12057
12157
  };
12058
12158
  }
12059
- /** Run the job's source to its end; a live source refused overloaded goes once more on a new socket. */
12159
+ /**
12160
+ * Run the job's source to its end. A live source refused overloaded goes once more on a new socket.
12161
+ * A source refused queue_full goes again once another source on the socket ended, so it may go after
12162
+ * jobs submitted later; with none of the endpoint's other sources open there, or with an upload from
12163
+ * the caller's own stream, which cannot be read twice, the refusal stands.
12164
+ */
12060
12165
  async run(job) {
12061
12166
  let retried = false;
12062
12167
  while (true) {
@@ -12067,9 +12172,9 @@ var PipelineBackend = class {
12067
12172
  const { socket, source } = sent;
12068
12173
  const uploading = source.uploads.size > 0 ? this.upload(socket, source, job) : Promise.resolve();
12069
12174
  let whip = null;
12070
- let overloaded;
12175
+ let refused;
12071
12176
  try {
12072
- overloaded = await this.follow(job, source, () => {
12177
+ refused = await this.follow(job, source, () => {
12073
12178
  if (job.mediaStream !== void 0) {
12074
12179
  const fail = (error) => {
12075
12180
  job.stopOnPipeline?.();
@@ -12087,9 +12192,19 @@ var PipelineBackend = class {
12087
12192
  await uploading;
12088
12193
  await whip?.close();
12089
12194
  }
12090
- if (!overloaded) {
12195
+ if (refused === null) {
12091
12196
  return;
12092
12197
  }
12198
+ if (refused === "queue_full") {
12199
+ if (socket.accepted.size === 0 && socket.ended === source.endedBefore) {
12200
+ throw new PipelineCommandError("queue_full", "the socket refused the source with none of its own open");
12201
+ }
12202
+ if (job.media.some((media) => media.once)) {
12203
+ throw new PipelineCommandError("queue_full", "the socket refused an upload from a stream, which cannot be sent again");
12204
+ }
12205
+ socket.cap = Math.min(socket.accepted.size, socket.cap ?? socket.accepted.size) || null;
12206
+ continue;
12207
+ }
12093
12208
  if (retried || !job.live) {
12094
12209
  throw new PipelineCommandError("overloaded", "the pipeline API has no capacity for another live source");
12095
12210
  }
@@ -12103,12 +12218,27 @@ var PipelineBackend = class {
12103
12218
  return null;
12104
12219
  }
12105
12220
  const combined = await combinedMedia(job);
12106
- const socket = await this.ensureSocket();
12221
+ let socket;
12222
+ while (true) {
12223
+ socket = await this.socketForJob(job);
12224
+ await socket.room(job);
12225
+ if (job.cancelled) {
12226
+ return null;
12227
+ }
12228
+ if (socket.open) {
12229
+ break;
12230
+ }
12231
+ }
12107
12232
  const source = new OpenSource(this.newId("source"));
12108
12233
  if (combined === null) {
12109
12234
  uploadKeys(job).forEach((key) => source.uploads.set(key, new Credit()));
12110
12235
  }
12111
12236
  socket.sources.set(source.id, source);
12237
+ socket.held.add(source.id);
12238
+ if (job.live) {
12239
+ socket.lives.add(source.id);
12240
+ }
12241
+ source.endedBefore = socket.ended;
12112
12242
  job.openSource = source;
12113
12243
  job.stopOnPipeline = () => {
12114
12244
  if (socket.open) {
@@ -12123,15 +12253,15 @@ var PipelineBackend = class {
12123
12253
  }
12124
12254
  return { socket, source };
12125
12255
  }
12126
- /** Hand the source's predictions to the job until it ends; true when the pipeline API was overloaded. */
12256
+ /** Hand the source's predictions to the job until it ends; the refusal to retry on, overloaded or queue_full. */
12127
12257
  async follow(job, source, onResult) {
12128
12258
  while (true) {
12129
12259
  if (job.cancelled) {
12130
- return false;
12260
+ return null;
12131
12261
  }
12132
12262
  const message = await source.inbox.next();
12133
12263
  if (message === CANCELLED) {
12134
- return false;
12264
+ return null;
12135
12265
  }
12136
12266
  if (message instanceof Error) {
12137
12267
  throw message;
@@ -12145,12 +12275,12 @@ var PipelineBackend = class {
12145
12275
  break;
12146
12276
  case "end":
12147
12277
  if (message["reason"] === "eos") {
12148
- return false;
12278
+ return null;
12149
12279
  }
12150
12280
  throw new PipelineSourceError(source.id, message["reason"] ?? "", message["message"] ?? "");
12151
12281
  case "error":
12152
- if (message["error"] === "overloaded") {
12153
- return true;
12282
+ if (message["error"] === "overloaded" || message["error"] === "queue_full") {
12283
+ return message["error"];
12154
12284
  }
12155
12285
  throw new PipelineCommandError(message["error"] ?? "", message["message"] ?? "");
12156
12286
  case "dropped":
@@ -12294,7 +12424,8 @@ function knownSize(body) {
12294
12424
  return void 0;
12295
12425
  }
12296
12426
  function bodyMedia(body, contentType, size) {
12297
- return { open: async () => body, contentType, size: size ?? knownSize(body) };
12427
+ const once = !(typeof Blob !== "undefined" && body instanceof Blob) && !(body instanceof ArrayBuffer) && !ArrayBuffer.isView(body);
12428
+ return { open: async () => body, contentType, size: size ?? knownSize(body), once };
12298
12429
  }
12299
12430
  function uploadGroup(media) {
12300
12431
  return { group: media.map((m) => ({ upload: { contentType: m.contentType } })) };
@@ -12382,6 +12513,9 @@ var WorkerEndpoint = class extends Endpoint {
12382
12513
  if (options.jobQueueLength > MAX_OPEN_SOURCES) {
12383
12514
  throw new PipelineConfigurationError("job_queue_length", `jobQueueLength ${options.jobQueueLength} exceeds ${MAX_OPEN_SOURCES}, the open sources a pipeline API socket takes`);
12384
12515
  }
12516
+ if (options.pipelineSockets !== void 0 && !(Number.isInteger(options.pipelineSockets) && options.pipelineSockets > 0)) {
12517
+ throw new PipelineConfigurationError("pipeline_sockets", `pipelineSockets ${options.pipelineSockets} is not a positive whole number`);
12518
+ }
12385
12519
  }
12386
12520
  super(options);
12387
12521
  const context = {
@@ -14356,6 +14490,7 @@ export {
14356
14490
  ModelType,
14357
14491
  MotionModel,
14358
14492
  PIPELINE_PATH,
14493
+ PIPELINE_SOCKETS,
14359
14494
  PipelineBackend,
14360
14495
  PipelineCommandError,
14361
14496
  PipelineConfigurationError,