@eyepop.ai/eyepop 3.25.2 → 3.26.0-rc.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.
@@ -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,
@@ -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"]));
@@ -12060,15 +12144,28 @@ var PipelineBackend = class {
12060
12144
  pop() {
12061
12145
  return this._currentPop;
12062
12146
  }
12063
- /** Set the pop on the socket's pipeline; a pop it refuses rejects, and the last pop it took stays. */
12147
+ /**
12148
+ * Set the pop on every open socket's pipeline; a pop the first refuses rejects, and the last pop it
12149
+ * took stays. shortcut: a refusal from a later socket, which the first took, leaves the earlier ones
12150
+ * on the new pop; a refusal is the pop's own fault, so every socket answers it alike.
12151
+ */
12064
12152
  async changePop(pop) {
12065
12153
  if (this.ctx.client() === null) {
12066
12154
  this._currentPop = pop;
12067
12155
  return;
12068
12156
  }
12069
12157
  await this.inOrder(async () => {
12070
- if (this._socket !== null && this._socket.open) {
12071
- await this.setPopOn(this._socket, pop);
12158
+ const sockets = this.openSockets();
12159
+ if (sockets.length > 0) {
12160
+ for (const socket of sockets) {
12161
+ try {
12162
+ await this.setPopOn(socket, pop);
12163
+ } catch (error) {
12164
+ if (!(error instanceof PipelineSocketClosedError)) {
12165
+ throw error;
12166
+ }
12167
+ }
12168
+ }
12072
12169
  this._currentPop = pop;
12073
12170
  return;
12074
12171
  }
@@ -12171,7 +12268,12 @@ var PipelineBackend = class {
12171
12268
  }
12172
12269
  };
12173
12270
  }
12174
- /** Run the job's source to its end; a live source refused overloaded goes once more on a new socket. */
12271
+ /**
12272
+ * Run the job's source to its end. A live source refused overloaded goes once more on a new socket.
12273
+ * A source refused queue_full goes again once another source on the socket ended, so it may go after
12274
+ * jobs submitted later; with none of the endpoint's other sources open there, or with an upload from
12275
+ * the caller's own stream, which cannot be read twice, the refusal stands.
12276
+ */
12175
12277
  async run(job) {
12176
12278
  let retried = false;
12177
12279
  while (true) {
@@ -12182,9 +12284,9 @@ var PipelineBackend = class {
12182
12284
  const { socket, source } = sent;
12183
12285
  const uploading = source.uploads.size > 0 ? this.upload(socket, source, job) : Promise.resolve();
12184
12286
  let whip = null;
12185
- let overloaded;
12287
+ let refused;
12186
12288
  try {
12187
- overloaded = await this.follow(job, source, () => {
12289
+ refused = await this.follow(job, source, () => {
12188
12290
  if (job.mediaStream !== void 0) {
12189
12291
  const fail = (error) => {
12190
12292
  job.stopOnPipeline?.();
@@ -12202,9 +12304,19 @@ var PipelineBackend = class {
12202
12304
  await uploading;
12203
12305
  await whip?.close();
12204
12306
  }
12205
- if (!overloaded) {
12307
+ if (refused === null) {
12206
12308
  return;
12207
12309
  }
12310
+ if (refused === "queue_full") {
12311
+ if (socket.accepted.size === 0 && socket.ended === source.endedBefore) {
12312
+ throw new PipelineCommandError("queue_full", "the socket refused the source with none of its own open");
12313
+ }
12314
+ if (job.media.some((media) => media.once)) {
12315
+ throw new PipelineCommandError("queue_full", "the socket refused an upload from a stream, which cannot be sent again");
12316
+ }
12317
+ socket.cap = Math.min(socket.accepted.size, socket.cap ?? socket.accepted.size) || null;
12318
+ continue;
12319
+ }
12208
12320
  if (retried || !job.live) {
12209
12321
  throw new PipelineCommandError("overloaded", "the pipeline API has no capacity for another live source");
12210
12322
  }
@@ -12218,12 +12330,27 @@ var PipelineBackend = class {
12218
12330
  return null;
12219
12331
  }
12220
12332
  const combined = await combinedMedia(job);
12221
- const socket = await this.ensureSocket();
12333
+ let socket;
12334
+ while (true) {
12335
+ socket = await this.socketForJob(job);
12336
+ await socket.room(job);
12337
+ if (job.cancelled) {
12338
+ return null;
12339
+ }
12340
+ if (socket.open) {
12341
+ break;
12342
+ }
12343
+ }
12222
12344
  const source = new OpenSource(this.newId("source"));
12223
12345
  if (combined === null) {
12224
12346
  uploadKeys(job).forEach((key) => source.uploads.set(key, new Credit()));
12225
12347
  }
12226
12348
  socket.sources.set(source.id, source);
12349
+ socket.held.add(source.id);
12350
+ if (job.live) {
12351
+ socket.lives.add(source.id);
12352
+ }
12353
+ source.endedBefore = socket.ended;
12227
12354
  job.openSource = source;
12228
12355
  job.stopOnPipeline = () => {
12229
12356
  if (socket.open) {
@@ -12238,15 +12365,15 @@ var PipelineBackend = class {
12238
12365
  }
12239
12366
  return { socket, source };
12240
12367
  }
12241
- /** Hand the source's predictions to the job until it ends; true when the pipeline API was overloaded. */
12368
+ /** Hand the source's predictions to the job until it ends; the refusal to retry on, overloaded or queue_full. */
12242
12369
  async follow(job, source, onResult) {
12243
12370
  while (true) {
12244
12371
  if (job.cancelled) {
12245
- return false;
12372
+ return null;
12246
12373
  }
12247
12374
  const message = await source.inbox.next();
12248
12375
  if (message === CANCELLED) {
12249
- return false;
12376
+ return null;
12250
12377
  }
12251
12378
  if (message instanceof Error) {
12252
12379
  throw message;
@@ -12260,12 +12387,12 @@ var PipelineBackend = class {
12260
12387
  break;
12261
12388
  case "end":
12262
12389
  if (message["reason"] === "eos") {
12263
- return false;
12390
+ return null;
12264
12391
  }
12265
12392
  throw new PipelineSourceError(source.id, message["reason"] ?? "", message["message"] ?? "");
12266
12393
  case "error":
12267
- if (message["error"] === "overloaded") {
12268
- return true;
12394
+ if (message["error"] === "overloaded" || message["error"] === "queue_full") {
12395
+ return message["error"];
12269
12396
  }
12270
12397
  throw new PipelineCommandError(message["error"] ?? "", message["message"] ?? "");
12271
12398
  case "dropped":
@@ -12409,7 +12536,8 @@ function knownSize(body) {
12409
12536
  return void 0;
12410
12537
  }
12411
12538
  function bodyMedia(body, contentType, size) {
12412
- return { open: async () => body, contentType, size: size ?? knownSize(body) };
12539
+ const once = !(typeof Blob !== "undefined" && body instanceof Blob) && !(body instanceof ArrayBuffer) && !ArrayBuffer.isView(body);
12540
+ return { open: async () => body, contentType, size: size ?? knownSize(body), once };
12413
12541
  }
12414
12542
  function uploadGroup(media) {
12415
12543
  return { group: media.map((m) => ({ upload: { contentType: m.contentType } })) };
@@ -12497,6 +12625,9 @@ var WorkerEndpoint = class extends Endpoint {
12497
12625
  if (options.jobQueueLength > MAX_OPEN_SOURCES) {
12498
12626
  throw new PipelineConfigurationError("job_queue_length", `jobQueueLength ${options.jobQueueLength} exceeds ${MAX_OPEN_SOURCES}, the open sources a pipeline API socket takes`);
12499
12627
  }
12628
+ if (options.pipelineSockets !== void 0 && !(Number.isInteger(options.pipelineSockets) && options.pipelineSockets > 0)) {
12629
+ throw new PipelineConfigurationError("pipeline_sockets", `pipelineSockets ${options.pipelineSockets} is not a positive whole number`);
12630
+ }
12500
12631
  }
12501
12632
  super(options);
12502
12633
  const context = {
@@ -14472,6 +14603,7 @@ var index_default = EyePop;
14472
14603
  ModelType,
14473
14604
  MotionModel,
14474
14605
  PIPELINE_PATH,
14606
+ PIPELINE_SOCKETS,
14475
14607
  PipelineBackend,
14476
14608
  PipelineCommandError,
14477
14609
  PipelineConfigurationError,