@eyepop.ai/eyepop 3.25.3 → 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.
@@ -11497,6 +11497,8 @@ if (isBrowser) {
11497
11497
  var PIPELINE_PATH = "/api/v1/pipeline";
11498
11498
  var SUBPROTOCOL = "eyepop.v1";
11499
11499
  var MAX_OPEN_SOURCES = 256;
11500
+ var PIPELINE_SOCKETS = 4;
11501
+ var EXTRA_SOCKET_RETRY_MS = 10 * 1e3;
11500
11502
  var COMBINED_UPLOAD_MAX_BYTES = 8 * 1024 * 1024;
11501
11503
  var CHUNK = 1;
11502
11504
  var COMBINED = 2;
@@ -11616,6 +11618,8 @@ var PipelineSocket = class {
11616
11618
  // The ids of sources sent on this socket that have neither ended nor been refused, as the
11617
11619
  // pipeline API counts them, cancelled ones included.
11618
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();
11619
11623
  // The held sources the pipeline API took: only these end, so only these make room.
11620
11624
  this.accepted = /* @__PURE__ */ new Set();
11621
11625
  // The ends on this socket so far.
@@ -11624,6 +11628,7 @@ var PipelineSocket = class {
11624
11628
  // the next send waits. Null once none is held.
11625
11629
  this.cap = null;
11626
11630
  this._roomWaiters = [];
11631
+ this.heartbeat = null;
11627
11632
  this.closed = new Promise((resolve) => this._markClosed = resolve);
11628
11633
  }
11629
11634
  /** A source ended or the socket closed: wake the send waiting for room. */
@@ -11657,6 +11662,10 @@ var PipelineSocket = class {
11657
11662
  return !this._closed && this.ws.readyState === 1;
11658
11663
  }
11659
11664
  markClosed() {
11665
+ if (this.heartbeat !== null) {
11666
+ clearInterval(this.heartbeat);
11667
+ this.heartbeat = null;
11668
+ }
11660
11669
  this._closed = true;
11661
11670
  this._markClosed();
11662
11671
  this.freed();
@@ -11745,14 +11754,14 @@ var PipelineBackend = class {
11745
11754
  constructor(ctx) {
11746
11755
  this.ctx = ctx;
11747
11756
  this._pipelineApiUrl = null;
11748
- this._socket = null;
11757
+ this._sockets = [];
11749
11758
  this._connecting = null;
11750
11759
  // Every job's setSource and every changePop goes out through this chain, in the order submitted.
11751
11760
  this._sendChain = Promise.resolve();
11752
11761
  this._disconnecting = false;
11753
11762
  this._nextId = 0;
11754
11763
  this._warned = /* @__PURE__ */ new Set();
11755
- this._heartbeat = null;
11764
+ this._noExtraSocketUntil = 0;
11756
11765
  // Reconnects at once since a source last played.
11757
11766
  this._reconnects = 0;
11758
11767
  this._currentPop = ctx.options.pop ?? null;
@@ -11766,13 +11775,9 @@ var PipelineBackend = class {
11766
11775
  async disconnect(closeEndpoint) {
11767
11776
  await closeEndpoint();
11768
11777
  this._disconnecting = true;
11769
- if (this._heartbeat !== null) {
11770
- clearInterval(this._heartbeat);
11771
- this._heartbeat = null;
11772
- }
11773
- const socket = this._socket;
11774
- this._socket = null;
11775
- if (socket !== null) {
11778
+ const sockets = this._sockets;
11779
+ this._sockets = [];
11780
+ for (const socket of sockets) {
11776
11781
  socket.ws.close(1e3);
11777
11782
  await socket.closed;
11778
11783
  }
@@ -11790,21 +11795,55 @@ var PipelineBackend = class {
11790
11795
  this._sendChain = result.catch(() => void 0);
11791
11796
  return result;
11792
11797
  }
11793
- /** 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. */
11794
11802
  async ensureSocket() {
11795
- if (this._socket !== null && this._socket.open) {
11796
- return this._socket;
11803
+ const open = this.openSockets()[0];
11804
+ if (open !== void 0) {
11805
+ return open;
11797
11806
  }
11798
11807
  if (this._connecting === null) {
11799
- this._connecting = this.openSocket().finally(() => this._connecting = null);
11808
+ this._connecting = this.openSocket(true).finally(() => this._connecting = null);
11800
11809
  }
11801
11810
  return await this._connecting;
11802
11811
  }
11803
- 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) {
11804
11839
  if (this.ctx.client() === null) {
11805
11840
  throw new PipelineSocketClosedError(void 0, "the endpoint is not connected");
11806
11841
  }
11807
- 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
+ }
11808
11847
  const socket = new PipelineSocket(ws);
11809
11848
  ws.onmessage = (event) => {
11810
11849
  if (typeof event.data === "string") {
@@ -11818,31 +11857,29 @@ var PipelineBackend = class {
11818
11857
  ws.onerror = () => {
11819
11858
  };
11820
11859
  ws.onclose = (event) => this.closed(socket, event.code, event.reason);
11821
- this._socket = socket;
11822
- this.startHeartbeat(ws);
11860
+ this._sockets.push(socket);
11861
+ this.startHeartbeat(socket);
11823
11862
  if (this._currentPop !== null) {
11824
11863
  try {
11825
11864
  await this.setPopOn(socket, this._currentPop);
11826
11865
  } catch (error) {
11827
- this._socket = null;
11866
+ this._sockets = this._sockets.filter((other) => other !== socket);
11828
11867
  ws.close(1e3);
11829
11868
  throw error;
11830
11869
  }
11831
11870
  }
11832
11871
  return socket;
11833
11872
  }
11834
- startHeartbeat(ws) {
11835
- if (this._heartbeat !== null) {
11836
- clearInterval(this._heartbeat);
11837
- }
11873
+ startHeartbeat(socket) {
11874
+ const ws = socket.ws;
11838
11875
  const ping = ws.ping;
11839
11876
  if (typeof ping === "function") {
11840
- this._heartbeat = setInterval(() => {
11877
+ socket.heartbeat = setInterval(() => {
11841
11878
  if (ws.readyState === 1) {
11842
11879
  ping.call(ws);
11843
11880
  }
11844
11881
  }, HEARTBEAT_MS);
11845
- this._heartbeat.unref?.();
11882
+ socket.heartbeat.unref?.();
11846
11883
  }
11847
11884
  }
11848
11885
  async socketUrl() {
@@ -11880,7 +11917,7 @@ var PipelineBackend = class {
11880
11917
  return (await this.ctx.credentialHeader()).replace(/^Bearer /, "");
11881
11918
  }
11882
11919
  /** Open the socket, waiting through 503 with Retry-After and exchanging the API key again after a 401. */
11883
- async upgrade(url) {
11920
+ async upgrade(url, patient) {
11884
11921
  const deadline = Date.now() + MAX_RETRY_MS;
11885
11922
  let rejected = false;
11886
11923
  while (true) {
@@ -11898,7 +11935,7 @@ var PipelineBackend = class {
11898
11935
  continue;
11899
11936
  }
11900
11937
  const wait = error.status === 503 && error.retryAfter !== void 0 ? Math.max(error.retryAfter, 1) * 1e3 : void 0;
11901
- if (wait === void 0 || Date.now() + wait > deadline) {
11938
+ if (!patient || wait === void 0 || Date.now() + wait > deadline) {
11902
11939
  throw error;
11903
11940
  }
11904
11941
  this.ctx.requestLogger.info("the pipeline API answered 503, retrying in %d ms", wait);
@@ -11908,9 +11945,7 @@ var PipelineBackend = class {
11908
11945
  }
11909
11946
  closed(socket, code, reason) {
11910
11947
  socket.markClosed();
11911
- if (this._socket === socket) {
11912
- this._socket = null;
11913
- }
11948
+ this._sockets = this._sockets.filter((other) => other !== socket);
11914
11949
  const error = new PipelineSocketClosedError(code, reason);
11915
11950
  socket.sources.forEach((source) => source.close(error));
11916
11951
  socket.commands.forEach((command) => command.reject(error));
@@ -11936,12 +11971,14 @@ var PipelineBackend = class {
11936
11971
  const source = typeof id === "string" ? socket.sources.get(id) : void 0;
11937
11972
  if (type === "end") {
11938
11973
  socket.held.delete(id);
11974
+ socket.lives.delete(id);
11939
11975
  socket.accepted.delete(id);
11940
11976
  socket.freed();
11941
11977
  } else if (type === "result" && socket.held.has(id)) {
11942
11978
  socket.accepted.add(id);
11943
11979
  } else if (type === "error" && message["error"] !== "unknown_command") {
11944
11980
  socket.held.delete(id);
11981
+ socket.lives.delete(id);
11945
11982
  socket.wake();
11946
11983
  }
11947
11984
  if (type === "send" || type === "credit") {
@@ -11991,15 +12028,28 @@ var PipelineBackend = class {
11991
12028
  pop() {
11992
12029
  return this._currentPop;
11993
12030
  }
11994
- /** Set the pop on the socket's pipeline; a pop it refuses rejects, and the last pop it took stays. */
12031
+ /**
12032
+ * Set the pop on every open socket's pipeline; a pop the first refuses rejects, and the last pop it
12033
+ * took stays. shortcut: a refusal from a later socket, which the first took, leaves the earlier ones
12034
+ * on the new pop; a refusal is the pop's own fault, so every socket answers it alike.
12035
+ */
11995
12036
  async changePop(pop) {
11996
12037
  if (this.ctx.client() === null) {
11997
12038
  this._currentPop = pop;
11998
12039
  return;
11999
12040
  }
12000
12041
  await this.inOrder(async () => {
12001
- if (this._socket !== null && this._socket.open) {
12002
- await this.setPopOn(this._socket, pop);
12042
+ const sockets = this.openSockets();
12043
+ if (sockets.length > 0) {
12044
+ for (const socket of sockets) {
12045
+ try {
12046
+ await this.setPopOn(socket, pop);
12047
+ } catch (error) {
12048
+ if (!(error instanceof PipelineSocketClosedError)) {
12049
+ throw error;
12050
+ }
12051
+ }
12052
+ }
12003
12053
  this._currentPop = pop;
12004
12054
  return;
12005
12055
  }
@@ -12160,22 +12210,30 @@ var PipelineBackend = class {
12160
12210
  }
12161
12211
  /** Send the job's setSource; the caller runs it in submission order. */
12162
12212
  async setSource(job) {
12163
- if (job.cancelled) {
12164
- return null;
12165
- }
12166
- let socket = await this.ensureSocket();
12167
- await socket.room(job);
12168
12213
  if (job.cancelled) {
12169
12214
  return null;
12170
12215
  }
12171
12216
  const combined = await combinedMedia(job);
12172
- socket = await this.ensureSocket();
12217
+ let socket;
12218
+ while (true) {
12219
+ socket = await this.socketForJob(job);
12220
+ await socket.room(job);
12221
+ if (job.cancelled) {
12222
+ return null;
12223
+ }
12224
+ if (socket.open) {
12225
+ break;
12226
+ }
12227
+ }
12173
12228
  const source = new OpenSource(this.newId("source"));
12174
12229
  if (combined === null) {
12175
12230
  uploadKeys(job).forEach((key) => source.uploads.set(key, new Credit()));
12176
12231
  }
12177
12232
  socket.sources.set(source.id, source);
12178
12233
  socket.held.add(source.id);
12234
+ if (job.live) {
12235
+ socket.lives.add(source.id);
12236
+ }
12179
12237
  source.endedBefore = socket.ended;
12180
12238
  job.openSource = source;
12181
12239
  job.stopOnPipeline = () => {
@@ -12451,6 +12509,9 @@ var WorkerEndpoint = class extends Endpoint {
12451
12509
  if (options.jobQueueLength > MAX_OPEN_SOURCES) {
12452
12510
  throw new PipelineConfigurationError("job_queue_length", `jobQueueLength ${options.jobQueueLength} exceeds ${MAX_OPEN_SOURCES}, the open sources a pipeline API socket takes`);
12453
12511
  }
12512
+ if (options.pipelineSockets !== void 0 && !(Number.isInteger(options.pipelineSockets) && options.pipelineSockets > 0)) {
12513
+ throw new PipelineConfigurationError("pipeline_sockets", `pipelineSockets ${options.pipelineSockets} is not a positive whole number`);
12514
+ }
12454
12515
  }
12455
12516
  super(options);
12456
12517
  const context = {
@@ -14425,6 +14486,7 @@ export {
14425
14486
  ModelType,
14426
14487
  MotionModel,
14427
14488
  PIPELINE_PATH,
14489
+ PIPELINE_SOCKETS,
14428
14490
  PipelineBackend,
14429
14491
  PipelineCommandError,
14430
14492
  PipelineConfigurationError,