@eyepop.ai/eyepop 3.23.0 → 3.25.1

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.
@@ -8666,6 +8666,7 @@ __export(index_exports, {
8666
8666
  AssetUrlType: () => AssetUrlType,
8667
8667
  AutoAnnotateStatus: () => AutoAnnotateStatus,
8668
8668
  AutoAnnotateTask: () => AutoAnnotateTask,
8669
+ COMBINED_UPLOAD_MAX_BYTES: () => COMBINED_UPLOAD_MAX_BYTES,
8669
8670
  ChangeType: () => ChangeType,
8670
8671
  ComputeAccountMismatchError: () => ComputeAccountMismatchError,
8671
8672
  ComputeApiError: () => ComputeApiError,
@@ -8687,6 +8688,7 @@ __export(index_exports, {
8687
8688
  LoadFromGroupJob: () => LoadFromGroupJob,
8688
8689
  LoadFromJob: () => LoadFromJob,
8689
8690
  LoadMediaStreamJob: () => LoadMediaStreamJob,
8691
+ MAX_OPEN_SOURCES: () => MAX_OPEN_SOURCES,
8690
8692
  ModelExportStatus: () => ModelExportStatus,
8691
8693
  ModelFormat: () => ModelFormat,
8692
8694
  ModelPrecisionType: () => ModelPrecisionType,
@@ -8695,12 +8697,21 @@ __export(index_exports, {
8695
8697
  ModelTrainingStage: () => ModelTrainingStage,
8696
8698
  ModelType: () => ModelType,
8697
8699
  MotionModel: () => MotionModel,
8700
+ PIPELINE_PATH: () => PIPELINE_PATH,
8701
+ PipelineBackend: () => PipelineBackend,
8702
+ PipelineCommandError: () => PipelineCommandError,
8703
+ PipelineConfigurationError: () => PipelineConfigurationError,
8704
+ PipelineJob: () => PipelineJob,
8705
+ PipelineSocketClosedError: () => PipelineSocketClosedError,
8706
+ PipelineSourceError: () => PipelineSourceError,
8698
8707
  PointCloud: () => PointCloud,
8699
8708
  PopComponentType: () => PopComponentType,
8700
8709
  PredictionVersion: () => PredictionVersion,
8701
8710
  QUATERNION_TOLERANCE: () => QUATERNION_TOLERANCE,
8711
+ SUBPROTOCOL: () => SUBPROTOCOL,
8702
8712
  SelectMode: () => SelectMode,
8703
8713
  Semaphore: () => Semaphore,
8714
+ SessionBackend: () => SessionBackend,
8704
8715
  Stream: () => Stream,
8705
8716
  TaskMode: () => TaskMode,
8706
8717
  TaskType: () => TaskType,
@@ -8770,6 +8781,667 @@ function accessTokenCredential(auth) {
8770
8781
  return nonEmptyCredential(auth.accessToken);
8771
8782
  }
8772
8783
 
8784
+ // types.ts
8785
+ var EndpointState = /* @__PURE__ */ ((EndpointState2) => {
8786
+ EndpointState2["Idle"] = "Idle";
8787
+ EndpointState2["Busy"] = "Busy";
8788
+ EndpointState2["Authenticating"] = "Authenticating";
8789
+ EndpointState2["FetchConfig"] = "FetchConfig";
8790
+ EndpointState2["Error"] = "Error";
8791
+ EndpointState2["NotAvailable"] = "NotAvailable";
8792
+ return EndpointState2;
8793
+ })(EndpointState || {});
8794
+
8795
+ // shims/http_client.ts
8796
+ var createHttpClient;
8797
+ function logLineFromRequest(input, init) {
8798
+ const url = input instanceof Request ? input.url : input;
8799
+ const method = input instanceof Request ? input.method : init?.method || (init?.body !== void 0 ? "POST" : "GET");
8800
+ return `${method} ${url}`;
8801
+ }
8802
+ if ("document" in globalThis && "implementation" in globalThis.document) {
8803
+ class BrowserHttpClient {
8804
+ constructor(logger) {
8805
+ this._logger = logger;
8806
+ }
8807
+ async fetch(input, init) {
8808
+ const logLine = logLineFromRequest(input, init);
8809
+ this._logger.debug(`Browser fetch() BEFORE ${logLine}`);
8810
+ const response = await fetch(input, init);
8811
+ this._logger.debug(`AFTER ${logLine} -> ${response.status}`);
8812
+ return response;
8813
+ }
8814
+ async close() {
8815
+ }
8816
+ isFullDuplex() {
8817
+ return false;
8818
+ }
8819
+ }
8820
+ createHttpClient = async (logger) => {
8821
+ return new BrowserHttpClient(logger);
8822
+ };
8823
+ } else {
8824
+ class NodeHttpClient {
8825
+ constructor(agent, fetchFn, logger) {
8826
+ this._agent = agent;
8827
+ this._fetch = fetchFn;
8828
+ this._logger = logger;
8829
+ }
8830
+ async fetch(input, init) {
8831
+ init || (init = {});
8832
+ init.dispatcher = this._agent;
8833
+ const logLine = logLineFromRequest(input, init);
8834
+ this._logger.debug(`Node fetch() BEFORE ${logLine}`);
8835
+ const response = await this._fetch(input, init);
8836
+ this._logger.debug(`AFTER ${logLine} -> ${response.status}`);
8837
+ return response;
8838
+ }
8839
+ async close() {
8840
+ return this._agent.close();
8841
+ }
8842
+ isFullDuplex() {
8843
+ return false;
8844
+ }
8845
+ }
8846
+ createHttpClient = async (logger) => {
8847
+ const undici = require("undici");
8848
+ const agent = new undici.Agent({
8849
+ keepAliveTimeout: 1e4,
8850
+ connections: 5,
8851
+ // two parallel streaming requests to the same host seem to deadlock with pipelining > 0
8852
+ pipelining: 0
8853
+ });
8854
+ logger.debug("Using node fetch with undici agent as http client");
8855
+ return new NodeHttpClient(agent, undici.fetch, logger);
8856
+ };
8857
+ }
8858
+
8859
+ // semaphore.ts
8860
+ var Semaphore = class {
8861
+ /**
8862
+ * Creates a semaphore.
8863
+ * @param permits The number of permits, i.e. strands of execution being allowed
8864
+ * to run in parallel.
8865
+ * This number can be initialized with a negative integer.
8866
+ */
8867
+ constructor(permits) {
8868
+ this.promiseResolverQueue = [];
8869
+ this.permits = permits;
8870
+ }
8871
+ /**
8872
+ * Returns the number of available permits.
8873
+ * @returns The number of available permits.
8874
+ */
8875
+ getPermits() {
8876
+ return this.permits;
8877
+ }
8878
+ /**
8879
+ * Returns a promise used to wait for a permit to become available. This method should be awaited on.
8880
+ * @returns A promise that gets resolved when execution is allowed to proceed.
8881
+ */
8882
+ async wait() {
8883
+ if (this.permits > 0) {
8884
+ this.permits -= 1;
8885
+ return Promise.resolve(true);
8886
+ }
8887
+ return new Promise((resolver) => this.promiseResolverQueue.push(resolver));
8888
+ }
8889
+ /**
8890
+ * Alias for {@linkcode Semaphore.wait}.
8891
+ * @returns A promise that gets resolved when execution is allowed to proceed.
8892
+ */
8893
+ async acquire() {
8894
+ return this.wait();
8895
+ }
8896
+ /**
8897
+ * Same as {@linkcode Semaphore.wait} except the promise returned gets resolved with false if no
8898
+ * permit becomes available in time.
8899
+ * @param milliseconds The time spent waiting before the wait is aborted. This is a lower bound,
8900
+ * you shouldn't rely on it being precise.
8901
+ * @returns A promise that gets resolved to true when execution is allowed to proceed or
8902
+ * false if the time given elapses before a permit becomes available.
8903
+ */
8904
+ async waitFor(milliseconds) {
8905
+ if (this.permits > 0) {
8906
+ this.permits -= 1;
8907
+ return Promise.resolve(true);
8908
+ }
8909
+ let resolver = (_) => void 0;
8910
+ const promise = new Promise((r) => {
8911
+ resolver = r;
8912
+ });
8913
+ this.promiseResolverQueue.push(resolver);
8914
+ setTimeout(() => {
8915
+ const index = this.promiseResolverQueue.indexOf(resolver);
8916
+ if (index !== -1) {
8917
+ this.promiseResolverQueue.splice(index, 1);
8918
+ } else {
8919
+ console.warn(`Semaphore.waitFor couldn't find its promise resolver in the queue`);
8920
+ }
8921
+ resolver(false);
8922
+ }, milliseconds);
8923
+ return promise;
8924
+ }
8925
+ /**
8926
+ * Synchronous function that tries to acquire a permit and returns true if successful, false otherwise.
8927
+ * @returns Whether a permit could be acquired.
8928
+ */
8929
+ tryAcquire() {
8930
+ if (this.permits > 0) {
8931
+ this.permits -= 1;
8932
+ return true;
8933
+ }
8934
+ return false;
8935
+ }
8936
+ /**
8937
+ * Acquires all permits that are currently available and returns the number of acquired permits.
8938
+ * @returns Number of acquired permits.
8939
+ */
8940
+ drainPermits() {
8941
+ if (this.permits > 0) {
8942
+ const permitCount = this.permits;
8943
+ this.permits = 0;
8944
+ return permitCount;
8945
+ }
8946
+ return 0;
8947
+ }
8948
+ /**
8949
+ * Increases the number of permits by one. If there are other functions waiting, one of them will
8950
+ * continue to execute in a future iteration of the event loop.
8951
+ */
8952
+ signal() {
8953
+ this.permits += 1;
8954
+ if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
8955
+ console.warn("Semaphore.permits should never be > 0 when there is someone waiting.");
8956
+ } else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
8957
+ this.permits -= 1;
8958
+ const nextResolver = this.promiseResolverQueue.shift();
8959
+ if (nextResolver) {
8960
+ nextResolver(true);
8961
+ }
8962
+ }
8963
+ }
8964
+ /**
8965
+ * Alias for {@linkcode Semaphore.signal}.
8966
+ */
8967
+ release() {
8968
+ this.signal();
8969
+ }
8970
+ /**
8971
+ * Schedules func to be called once a permit becomes available.
8972
+ * Returns a promise that resolves to the return value of func.
8973
+ * @typeparam T The return type of func.
8974
+ * @param func The function to be executed.
8975
+ * @return A promise that gets resolved with the return value of the function.
8976
+ */
8977
+ async execute(func) {
8978
+ await this.wait();
8979
+ try {
8980
+ return await func();
8981
+ } finally {
8982
+ this.signal();
8983
+ }
8984
+ }
8985
+ };
8986
+
8987
+ // endpoint.ts
8988
+ var import_pino = require("pino");
8989
+
8990
+ // shims/browser_session.ts
8991
+ var import_auth0_spa_js = require("@auth0/auth0-spa-js");
8992
+ var authenticateBrowserSession;
8993
+ if ("document" in globalThis && "implementation" in globalThis.document) {
8994
+ authenticateBrowserSession = async (auth0, options) => {
8995
+ if (options.eyepopUrl == null) {
8996
+ return Promise.reject("options.eyepopUrl cannot be null");
8997
+ }
8998
+ const auth0ClientOptions = {
8999
+ clientId: auth0.clientId,
9000
+ domain: auth0.domain,
9001
+ authorizationParams: {
9002
+ scope: auth0.scope,
9003
+ audience: auth0.audience
9004
+ }
9005
+ };
9006
+ const auth0Client = await (0, import_auth0_spa_js.createAuth0Client)(auth0ClientOptions);
9007
+ if (!await auth0Client.isAuthenticated()) {
9008
+ await auth0Client.loginWithPopup();
9009
+ }
9010
+ const accessToken = await auth0Client.getTokenSilently();
9011
+ if (!accessToken) {
9012
+ return Promise.reject("auth0 login failed");
9013
+ }
9014
+ let session;
9015
+ const workerOptions = options;
9016
+ if (typeof workerOptions.popId != "undefined") {
9017
+ const workerSession = {
9018
+ eyepopUrl: options.eyepopUrl,
9019
+ accessToken,
9020
+ validUntil: Date.now() + 60 * 60 * 1e3,
9021
+ popId: workerOptions.popId,
9022
+ baseUrl: void 0,
9023
+ pipelineId: void 0,
9024
+ isPipelineMode: workerOptions.isPipelineMode,
9025
+ authenticationHeaders: () => {
9026
+ return { Authorization: `Bearer ${accessToken}` };
9027
+ }
9028
+ };
9029
+ session = workerSession;
9030
+ } else {
9031
+ session = {
9032
+ eyepopUrl: options.eyepopUrl,
9033
+ accessToken,
9034
+ validUntil: Date.now() + 60 * 60 * 1e3
9035
+ };
9036
+ }
9037
+ return session;
9038
+ };
9039
+ } else {
9040
+ authenticateBrowserSession = (_auth0, _options) => {
9041
+ return Promise.reject("auth0 login not supported server-side");
9042
+ };
9043
+ }
9044
+
9045
+ // endpoint.ts
9046
+ var Endpoint = class {
9047
+ constructor(options) {
9048
+ this._options = options;
9049
+ this._token = null;
9050
+ this._expire_token_time = null;
9051
+ this._client = null;
9052
+ this._statusRetryHandlers = /* @__PURE__ */ new Map();
9053
+ this._statusRetryHandlers.set(401, this.statusHandler401);
9054
+ this._limit = new Semaphore(this._options.jobQueueLength ?? 1024);
9055
+ this._state = "Idle" /* Idle */;
9056
+ this._message = null;
9057
+ this._stateChangeHandler = null;
9058
+ let rootLogger;
9059
+ if (options.logger) {
9060
+ rootLogger = options.logger;
9061
+ } else {
9062
+ rootLogger = (0, import_pino.pino)();
9063
+ }
9064
+ this._logger = rootLogger.child({ module: "eyepop" });
9065
+ this._requestLogger = this._logger.child({ module: "requests" });
9066
+ }
9067
+ setStatusRetryHandlers(statusRetryHandlers = void 0) {
9068
+ this._statusRetryHandlers = new Map(statusRetryHandlers);
9069
+ this._statusRetryHandlers.set(401, this.statusHandler401);
9070
+ }
9071
+ eyepopUrl() {
9072
+ if (this._options.eyepopUrl) {
9073
+ return this._options.eyepopUrl.replace(/\/+$/, "");
9074
+ } else {
9075
+ return "https://compute.eyepop.ai";
9076
+ }
9077
+ }
9078
+ // TODO: deprecated Web API endpoint needed for POP_ID support
9079
+ eyepopWebApiUrl() {
9080
+ if (this._options.eyepopUrl) {
9081
+ return this._options.eyepopUrl.replace(/\/+$/, "").replace(/compute\./, "web-api.");
9082
+ } else {
9083
+ return "https://api.eyepop.ai";
9084
+ }
9085
+ }
9086
+ pendingJobs() {
9087
+ return (this._options.jobQueueLength ?? 1024) - this._limit.getPermits();
9088
+ }
9089
+ state() {
9090
+ return this._state;
9091
+ }
9092
+ lastMessage() {
9093
+ return this._message;
9094
+ }
9095
+ onStateChanged(handler) {
9096
+ this._stateChangeHandler = handler;
9097
+ return this;
9098
+ }
9099
+ updateState(newState = void 0, message = void 0) {
9100
+ const fromState = this._state;
9101
+ if (newState == void 0) {
9102
+ if (this._limit.getPermits() == this._options.jobQueueLength) {
9103
+ newState = "Idle" /* Idle */;
9104
+ } else {
9105
+ newState = "Busy" /* Busy */;
9106
+ }
9107
+ }
9108
+ this._state = newState;
9109
+ this._message = message ? message : null;
9110
+ if (this._stateChangeHandler) {
9111
+ this._stateChangeHandler(fromState, this._state);
9112
+ }
9113
+ }
9114
+ async connect() {
9115
+ if (this._client) {
9116
+ this._logger.warn("endpoint already connected");
9117
+ return this;
9118
+ }
9119
+ if (!this._options.eyepopUrl) {
9120
+ return Promise.reject("option eyepopUrl or environment variable EYEPOP_URL is required");
9121
+ }
9122
+ const auth = resolveAuth(this._options);
9123
+ if (auth === void 0) {
9124
+ return Promise.reject("cannot connect without defined auth option");
9125
+ }
9126
+ if (auth.session !== void 0) {
9127
+ this._token = auth.session.accessToken;
9128
+ this._expire_token_time = auth.session.validUntil / 1e3;
9129
+ } else if (auth.oAuth2 !== void 0) {
9130
+ } else if (accessTokenCredential(auth) !== void 0) {
9131
+ } else if (apiKeyCredential(auth) !== void 0) {
9132
+ } else if (auth.isLocal !== void 0) {
9133
+ } else {
9134
+ return Promise.reject("option apiKey/accessToken or environment variable EYEPOP_API_KEY is required");
9135
+ }
9136
+ if (this._options.platformSupport?.createHttpClient !== void 0) {
9137
+ this._client = await this._options.platformSupport.createHttpClient(this._requestLogger);
9138
+ } else {
9139
+ this._client = await createHttpClient(this._requestLogger);
9140
+ }
9141
+ return await this.reconnect();
9142
+ }
9143
+ async disconnect(wait = true) {
9144
+ if (wait && this._limit) {
9145
+ for (let j = 0; j < this._options.jobQueueLength; j++) {
9146
+ await this._limit.acquire();
9147
+ }
9148
+ }
9149
+ this._limit = new Semaphore(this._options.jobQueueLength ?? 1024);
9150
+ if (this._client) {
9151
+ await this._client.close();
9152
+ this._client = null;
9153
+ }
9154
+ }
9155
+ async session() {
9156
+ await this.currentAccessToken();
9157
+ if (this._token == null || this._expire_token_time == null) {
9158
+ return Promise.reject("endpoint not connected");
9159
+ }
9160
+ return {
9161
+ eyepopUrl: this._options.eyepopUrl,
9162
+ accessToken: this._token,
9163
+ validUntil: this._expire_token_time * 1e3
9164
+ };
9165
+ }
9166
+ statusHandler401(_statusCode) {
9167
+ this._token = null;
9168
+ this._expire_token_time = null;
9169
+ }
9170
+ async fetchWithRetry(fetcher, retries = 1) {
9171
+ while (true) {
9172
+ try {
9173
+ const response = await fetcher();
9174
+ if (response.ok) {
9175
+ return response;
9176
+ } else {
9177
+ const handler = this._statusRetryHandlers.get(response.status);
9178
+ if (handler != null) {
9179
+ handler(response.status);
9180
+ if (retries--) {
9181
+ this._requestLogger.info("received %d response, retry", response.status);
9182
+ } else {
9183
+ return response;
9184
+ }
9185
+ } else {
9186
+ return response;
9187
+ }
9188
+ }
9189
+ } catch (error) {
9190
+ const handler = this._statusRetryHandlers.get(404);
9191
+ if (handler != null) {
9192
+ handler(error);
9193
+ if (retries--) {
9194
+ this._requestLogger.info("error %s, retry", error);
9195
+ } else {
9196
+ return Promise.reject(`error ${error}`);
9197
+ }
9198
+ } else {
9199
+ return Promise.reject(`error ${error}`);
9200
+ }
9201
+ }
9202
+ }
9203
+ }
9204
+ async reconnect() {
9205
+ throw new Error("reconnect() not implemented");
9206
+ }
9207
+ async authorizationHeader() {
9208
+ if (resolveAuth(this._options)?.isLocal !== void 0) {
9209
+ return `Implicit`;
9210
+ }
9211
+ return `Bearer ${await this.currentAccessToken()}`;
9212
+ }
9213
+ async currentAccessToken() {
9214
+ const now = Date.now() / 1e3;
9215
+ if (!this._token || this._expire_token_time < now) {
9216
+ this.updateState("Authenticating" /* Authenticating */);
9217
+ try {
9218
+ const auth = resolveAuth(this._options);
9219
+ if (!this._client) {
9220
+ return Promise.reject("endpoint not connected");
9221
+ } else if (auth?.isLocal !== void 0) {
9222
+ this._token = "implicit";
9223
+ this._expire_token_time = Number.MAX_VALUE;
9224
+ } else if (auth?.session !== void 0) {
9225
+ return Promise.reject("temporary access token expired");
9226
+ } else if (auth?.oAuth2 !== void 0 && this._options.eyepopUrl) {
9227
+ const session = await authenticateBrowserSession(auth.oAuth2, this._options);
9228
+ this._token = session.accessToken;
9229
+ this._expire_token_time = session.validUntil / 1e3;
9230
+ } else if (accessTokenCredential(auth) !== void 0) {
9231
+ this._token = accessTokenCredential(auth);
9232
+ this._expire_token_time = Number.MAX_VALUE;
9233
+ } else if (this._options.eyepopUrl) {
9234
+ const credential = apiKeyCredential(auth);
9235
+ if (credential === void 0) {
9236
+ return Promise.reject("no valid auth option");
9237
+ }
9238
+ const headers = { Authorization: `Bearer ${credential}` };
9239
+ const post_url = `${this.eyepopUrl()}/v1/auth/authenticate`;
9240
+ const response = await this._client.fetch(post_url, {
9241
+ method: "POST",
9242
+ headers
9243
+ });
9244
+ if (response.status != 200) {
9245
+ const message = await response.text();
9246
+ return Promise.reject(`Unexpected status ${response.status}: ${message}`);
9247
+ }
9248
+ const data = await response.json();
9249
+ const token = data;
9250
+ this._token = token.access_token;
9251
+ this._expire_token_time = now + token.expires_in - 60;
9252
+ } else {
9253
+ return Promise.reject("no valid auth option");
9254
+ }
9255
+ } finally {
9256
+ this.updateState();
9257
+ }
9258
+ }
9259
+ this._logger.debug("using access token, valid for at least %d seconds", this._expire_token_time - now);
9260
+ return this._token;
9261
+ }
9262
+ };
9263
+
9264
+ // camera.ts
9265
+ var QUATERNION_TOLERANCE = 1e-3;
9266
+ function isFinite_(value) {
9267
+ return value === void 0 || Number.isFinite(value);
9268
+ }
9269
+ function validateCamera(camera) {
9270
+ if (camera.intrinsics !== void 0 && camera.hfovDegrees !== void 0) {
9271
+ throw new Error("camera.intrinsics and camera.hfovDegrees are alternatives, supply one");
9272
+ }
9273
+ if (camera.intrinsics === void 0 && camera.hfovDegrees === void 0) {
9274
+ throw new Error("camera requires either camera.intrinsics or camera.hfovDegrees");
9275
+ }
9276
+ if (camera.hfovDegrees !== void 0) {
9277
+ if (!(camera.hfovDegrees > 0 && camera.hfovDegrees < 180)) {
9278
+ throw new Error("camera.hfovDegrees must be a horizontal field of view in (0, 180) degrees");
9279
+ }
9280
+ } else {
9281
+ const intrinsics = camera.intrinsics;
9282
+ for (const [name, focal] of [
9283
+ ["fx", intrinsics.fx],
9284
+ ["fy", intrinsics.fy]
9285
+ ]) {
9286
+ if (!(focal > 0) || !Number.isFinite(focal)) {
9287
+ throw new Error(`camera.intrinsics.${name} must be a positive, finite focal length`);
9288
+ }
9289
+ }
9290
+ for (const [name, principal, extent] of [
9291
+ ["cx", intrinsics.cx, "width"],
9292
+ ["cy", intrinsics.cy, "height"]
9293
+ ]) {
9294
+ if (!(principal >= 0 && principal <= 1)) {
9295
+ throw new Error(`camera.intrinsics.${name} must be within [0, 1] of the frame ${extent}`);
9296
+ }
9297
+ }
9298
+ }
9299
+ if (camera.distortion !== void 0) {
9300
+ for (const name of ["k1", "k2", "p1", "p2", "k3"]) {
9301
+ if (!isFinite_(camera.distortion[name])) {
9302
+ throw new Error(`camera.distortion.${name} must be a finite number`);
9303
+ }
9304
+ }
9305
+ }
9306
+ if (camera.extrinsics !== void 0) {
9307
+ const translation = camera.extrinsics.translation;
9308
+ if (translation !== void 0) {
9309
+ for (const axis of ["x", "y", "z"]) {
9310
+ if (!Number.isFinite(translation[axis])) {
9311
+ throw new Error(`camera.extrinsics.translation.${axis} must be a finite number`);
9312
+ }
9313
+ }
9314
+ }
9315
+ const rotation = camera.extrinsics.rotation;
9316
+ if (rotation !== void 0) {
9317
+ const norm = Math.sqrt(rotation.w ** 2 + rotation.x ** 2 + rotation.y ** 2 + rotation.z ** 2);
9318
+ if (!(Math.abs(norm - 1) <= QUATERNION_TOLERANCE)) {
9319
+ throw new Error("camera.extrinsics.rotation must be a unit quaternion");
9320
+ }
9321
+ }
9322
+ }
9323
+ }
9324
+
9325
+ // worker/worker_types.ts
9326
+ var PredictionVersion = /* @__PURE__ */ ((PredictionVersion3) => {
9327
+ PredictionVersion3[PredictionVersion3["V1"] = 1] = "V1";
9328
+ PredictionVersion3[PredictionVersion3["V2"] = 2] = "V2";
9329
+ PredictionVersion3[PredictionVersion3["V3"] = 3] = "V3";
9330
+ return PredictionVersion3;
9331
+ })(PredictionVersion || {});
9332
+ var DEFAULT_PREDICTION_VERSION = 2 /* V2 */;
9333
+ var VideoMode = /* @__PURE__ */ ((VideoMode2) => {
9334
+ VideoMode2["STREAM"] = "stream";
9335
+ VideoMode2["BUFFER"] = "buffer";
9336
+ return VideoMode2;
9337
+ })(VideoMode || {});
9338
+ var PopComponentType = /* @__PURE__ */ ((PopComponentType2) => {
9339
+ PopComponentType2["FORWARD"] = "forward";
9340
+ PopComponentType2["INFERENCE"] = "inference";
9341
+ PopComponentType2["TRACKING"] = "tracking";
9342
+ PopComponentType2["CONTOUR_FINDER"] = "contour_finder";
9343
+ PopComponentType2["COMPONENT_FINDER"] = "component_finder";
9344
+ return PopComponentType2;
9345
+ })(PopComponentType || {});
9346
+ var ForwardOperatorType = /* @__PURE__ */ ((ForwardOperatorType2) => {
9347
+ ForwardOperatorType2["FULL"] = "full";
9348
+ ForwardOperatorType2["CROP"] = "crop";
9349
+ ForwardOperatorType2["CROP_WITH_FULL_FALLBACK"] = "crop_with_full_fallback";
9350
+ ForwardOperatorType2["SELECT_CROP"] = "select_crop";
9351
+ ForwardOperatorType2["SELECT_FULL"] = "select_full";
9352
+ return ForwardOperatorType2;
9353
+ })(ForwardOperatorType || {});
9354
+ var SelectMode = /* @__PURE__ */ ((SelectMode2) => {
9355
+ SelectMode2["MOST_RELEVANT"] = "most_relevant";
9356
+ return SelectMode2;
9357
+ })(SelectMode || {});
9358
+ function isSelectOperator(operator) {
9359
+ return operator?.type === "select_crop" /* SELECT_CROP */ || operator?.type === "select_full" /* SELECT_FULL */;
9360
+ }
9361
+ var InferenceType = /* @__PURE__ */ ((InferenceType2) => {
9362
+ InferenceType2["IMAGE_CLASSIFICATION"] = "image_classification";
9363
+ InferenceType2["OBJECT_DETECTION"] = "object_detection";
9364
+ InferenceType2["KEY_POINTS"] = "key_points";
9365
+ InferenceType2["OCR"] = "ocr";
9366
+ InferenceType2["MESH"] = "mesh";
9367
+ InferenceType2["FEATURE_VECTOR"] = "feature_vector";
9368
+ InferenceType2["SEMANTIC_SEGMENTATION"] = "semantic_segmentation";
9369
+ InferenceType2["SEGMENTATION"] = "segmentation";
9370
+ return InferenceType2;
9371
+ })(InferenceType || {});
9372
+ var MotionModel = /* @__PURE__ */ ((MotionModel2) => {
9373
+ MotionModel2["RANDOM_WALK"] = "random_walk";
9374
+ MotionModel2["CONSTANT_VELOCITY"] = "constant_velocity";
9375
+ MotionModel2["CONSTANT_ACCELERATION"] = "constant_acceleration";
9376
+ return MotionModel2;
9377
+ })(MotionModel || {});
9378
+ var ContourType = /* @__PURE__ */ ((ContourType2) => {
9379
+ ContourType2["ALL_PIXELS"] = "all_pixels";
9380
+ ContourType2["POLYGON"] = "polygon";
9381
+ ContourType2["CONVEX_HULL"] = "convex_hull";
9382
+ ContourType2["HOUGH_CIRCLES"] = "hough_circles";
9383
+ ContourType2["CIRCLE"] = "circle";
9384
+ ContourType2["TRIANGLE"] = "triangle";
9385
+ ContourType2["RECTANGLE"] = "rectangle";
9386
+ return ContourType2;
9387
+ })(ContourType || {});
9388
+ function validatePop(pop) {
9389
+ if (pop.depthMap !== void 0) {
9390
+ if (pop.depthMap.ability === void 0 === (pop.depthMap.abilityUuid === void 0)) {
9391
+ throw new Error("depthMap requires exactly one of ability or abilityUuid");
9392
+ }
9393
+ }
9394
+ if (pop.defaults?.camera !== void 0) {
9395
+ validateCamera(pop.defaults.camera);
9396
+ }
9397
+ validateForwards(pop.components);
9398
+ }
9399
+ function popSelects(pop) {
9400
+ const selects = (components) => (components ?? []).some((component) => isSelectOperator(component.forward?.operator) || selects(component.forward?.targets));
9401
+ return selects(pop?.components);
9402
+ }
9403
+ function validateForwards(components) {
9404
+ for (const component of components ?? []) {
9405
+ const operator = component.forward?.operator;
9406
+ if (operator !== void 0) {
9407
+ validateSelectOperator(operator);
9408
+ }
9409
+ validateForwards(component.forward?.targets);
9410
+ }
9411
+ }
9412
+ function validateSelectOperator(operator) {
9413
+ if (!isSelectOperator(operator)) {
9414
+ if (operator.select !== void 0) {
9415
+ throw new Error("select is only valid with the select_crop or select_full operator");
9416
+ }
9417
+ return;
9418
+ }
9419
+ const select = operator.select;
9420
+ if (select === void 0) {
9421
+ throw new Error(`${operator.type} requires a select block`);
9422
+ }
9423
+ if (select.mode !== void 0 && select.mode !== "most_relevant" /* MOST_RELEVANT */) {
9424
+ throw new Error(`select mode ${select.mode} is not supported, the only mode is ${"most_relevant" /* MOST_RELEVANT */}`);
9425
+ }
9426
+ if (select.relevancyModel !== void 0 && select.relevancyModelUuid !== void 0) {
9427
+ throw new Error("select can only have one of relevancyModelUuid or relevancyModel");
9428
+ }
9429
+ if (select.minTrackLengthSeconds !== void 0 && select.minTrackLengthSeconds < 0) {
9430
+ throw new Error("select minTrackLengthSeconds cannot be negative");
9431
+ }
9432
+ if (select.intervalSeconds !== void 0 && select.intervalSeconds <= 0) {
9433
+ throw new Error("select intervalSeconds must be positive");
9434
+ }
9435
+ if (operator.crop !== void 0) {
9436
+ if (operator.type !== "select_crop" /* SELECT_CROP */) {
9437
+ throw new Error(`crop options are only valid with select_crop, not ${operator.type}`);
9438
+ }
9439
+ if (operator.crop.maxItems !== void 0) {
9440
+ throw new Error("select_crop forwards one detection per selection, maxItems does not apply");
9441
+ }
9442
+ }
9443
+ }
9444
+
8773
9445
  // compute/compute_session.ts
8774
9446
  var SESSION_DEAD = /* @__PURE__ */ new Set(["error" /* ERROR */, "failed" /* FAILED */, "stopped" /* STOPPED */, "pipeline_error" /* PIPELINE_ERROR */, "unknown" /* UNKNOWN */]);
8775
9447
  var CHECK_FOR_IS_READY_INTERVAL = 250;
@@ -9083,19 +9755,8 @@ var ComputeSessionClient = class {
9083
9755
  await sleep(CHECK_FOR_IS_READY_INTERVAL);
9084
9756
  }
9085
9757
  }
9086
- }
9087
- };
9088
-
9089
- // types.ts
9090
- var EndpointState = /* @__PURE__ */ ((EndpointState2) => {
9091
- EndpointState2["Idle"] = "Idle";
9092
- EndpointState2["Busy"] = "Busy";
9093
- EndpointState2["Authenticating"] = "Authenticating";
9094
- EndpointState2["FetchConfig"] = "FetchConfig";
9095
- EndpointState2["Error"] = "Error";
9096
- EndpointState2["NotAvailable"] = "NotAvailable";
9097
- return EndpointState2;
9098
- })(EndpointState || {});
9758
+ }
9759
+ };
9099
9760
 
9100
9761
  // streaming.ts
9101
9762
  var import_uuid = require("uuid");
@@ -9282,6 +9943,9 @@ function readableStreamFromString(s) {
9282
9943
  function randomMultipartBoundary() {
9283
9944
  return `----formdata-boundary-${(0, import_uuid.v4)()}`;
9284
9945
  }
9946
+ function escapeDispositionValue(value) {
9947
+ return value.replace(/"/g, "%22").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
9948
+ }
9285
9949
  function toPartStream(content) {
9286
9950
  if (content instanceof ReadableStream) {
9287
9951
  return content;
@@ -9326,8 +9990,11 @@ function encodeMultipartFormData(parts, boundary) {
9326
9990
  }
9327
9991
  const part = parts[partIndex];
9328
9992
  let header = `--${boundary}\r
9329
- Content-Disposition: form-data; name="${part.name}"\r
9330
- `;
9993
+ Content-Disposition: form-data; name="${escapeDispositionValue(part.name)}"`;
9994
+ if (part.filename !== void 0) {
9995
+ header += `; filename="${escapeDispositionValue(part.filename)}"`;
9996
+ }
9997
+ header += "\r\n";
9331
9998
  if (part.contentType) {
9332
9999
  header += `Content-Type: ${part.contentType}\r
9333
10000
  `;
@@ -9350,187 +10017,6 @@ Content-Disposition: form-data; name="${part.name}"\r
9350
10017
  });
9351
10018
  }
9352
10019
 
9353
- // camera.ts
9354
- var QUATERNION_TOLERANCE = 1e-3;
9355
- function isFinite_(value) {
9356
- return value === void 0 || Number.isFinite(value);
9357
- }
9358
- function validateCamera(camera) {
9359
- if (camera.intrinsics !== void 0 && camera.hfovDegrees !== void 0) {
9360
- throw new Error("camera.intrinsics and camera.hfovDegrees are alternatives, supply one");
9361
- }
9362
- if (camera.intrinsics === void 0 && camera.hfovDegrees === void 0) {
9363
- throw new Error("camera requires either camera.intrinsics or camera.hfovDegrees");
9364
- }
9365
- if (camera.hfovDegrees !== void 0) {
9366
- if (!(camera.hfovDegrees > 0 && camera.hfovDegrees < 180)) {
9367
- throw new Error("camera.hfovDegrees must be a horizontal field of view in (0, 180) degrees");
9368
- }
9369
- } else {
9370
- const intrinsics = camera.intrinsics;
9371
- for (const [name, focal] of [
9372
- ["fx", intrinsics.fx],
9373
- ["fy", intrinsics.fy]
9374
- ]) {
9375
- if (!(focal > 0) || !Number.isFinite(focal)) {
9376
- throw new Error(`camera.intrinsics.${name} must be a positive, finite focal length`);
9377
- }
9378
- }
9379
- for (const [name, principal, extent] of [
9380
- ["cx", intrinsics.cx, "width"],
9381
- ["cy", intrinsics.cy, "height"]
9382
- ]) {
9383
- if (!(principal >= 0 && principal <= 1)) {
9384
- throw new Error(`camera.intrinsics.${name} must be within [0, 1] of the frame ${extent}`);
9385
- }
9386
- }
9387
- }
9388
- if (camera.distortion !== void 0) {
9389
- for (const name of ["k1", "k2", "p1", "p2", "k3"]) {
9390
- if (!isFinite_(camera.distortion[name])) {
9391
- throw new Error(`camera.distortion.${name} must be a finite number`);
9392
- }
9393
- }
9394
- }
9395
- if (camera.extrinsics !== void 0) {
9396
- const translation = camera.extrinsics.translation;
9397
- if (translation !== void 0) {
9398
- for (const axis of ["x", "y", "z"]) {
9399
- if (!Number.isFinite(translation[axis])) {
9400
- throw new Error(`camera.extrinsics.translation.${axis} must be a finite number`);
9401
- }
9402
- }
9403
- }
9404
- const rotation = camera.extrinsics.rotation;
9405
- if (rotation !== void 0) {
9406
- const norm = Math.sqrt(rotation.w ** 2 + rotation.x ** 2 + rotation.y ** 2 + rotation.z ** 2);
9407
- if (!(Math.abs(norm - 1) <= QUATERNION_TOLERANCE)) {
9408
- throw new Error("camera.extrinsics.rotation must be a unit quaternion");
9409
- }
9410
- }
9411
- }
9412
- }
9413
-
9414
- // worker/worker_types.ts
9415
- var PredictionVersion = /* @__PURE__ */ ((PredictionVersion3) => {
9416
- PredictionVersion3[PredictionVersion3["V1"] = 1] = "V1";
9417
- PredictionVersion3[PredictionVersion3["V2"] = 2] = "V2";
9418
- PredictionVersion3[PredictionVersion3["V3"] = 3] = "V3";
9419
- return PredictionVersion3;
9420
- })(PredictionVersion || {});
9421
- var DEFAULT_PREDICTION_VERSION = 2 /* V2 */;
9422
- var VideoMode = /* @__PURE__ */ ((VideoMode2) => {
9423
- VideoMode2["STREAM"] = "stream";
9424
- VideoMode2["BUFFER"] = "buffer";
9425
- return VideoMode2;
9426
- })(VideoMode || {});
9427
- var PopComponentType = /* @__PURE__ */ ((PopComponentType2) => {
9428
- PopComponentType2["FORWARD"] = "forward";
9429
- PopComponentType2["INFERENCE"] = "inference";
9430
- PopComponentType2["TRACKING"] = "tracking";
9431
- PopComponentType2["CONTOUR_FINDER"] = "contour_finder";
9432
- PopComponentType2["COMPONENT_FINDER"] = "component_finder";
9433
- return PopComponentType2;
9434
- })(PopComponentType || {});
9435
- var ForwardOperatorType = /* @__PURE__ */ ((ForwardOperatorType2) => {
9436
- ForwardOperatorType2["FULL"] = "full";
9437
- ForwardOperatorType2["CROP"] = "crop";
9438
- ForwardOperatorType2["CROP_WITH_FULL_FALLBACK"] = "crop_with_full_fallback";
9439
- ForwardOperatorType2["SELECT_CROP"] = "select_crop";
9440
- ForwardOperatorType2["SELECT_FULL"] = "select_full";
9441
- return ForwardOperatorType2;
9442
- })(ForwardOperatorType || {});
9443
- var SelectMode = /* @__PURE__ */ ((SelectMode2) => {
9444
- SelectMode2["MOST_RELEVANT"] = "most_relevant";
9445
- return SelectMode2;
9446
- })(SelectMode || {});
9447
- function isSelectOperator(operator) {
9448
- return operator?.type === "select_crop" /* SELECT_CROP */ || operator?.type === "select_full" /* SELECT_FULL */;
9449
- }
9450
- var InferenceType = /* @__PURE__ */ ((InferenceType2) => {
9451
- InferenceType2["IMAGE_CLASSIFICATION"] = "image_classification";
9452
- InferenceType2["OBJECT_DETECTION"] = "object_detection";
9453
- InferenceType2["KEY_POINTS"] = "key_points";
9454
- InferenceType2["OCR"] = "ocr";
9455
- InferenceType2["MESH"] = "mesh";
9456
- InferenceType2["FEATURE_VECTOR"] = "feature_vector";
9457
- InferenceType2["SEMANTIC_SEGMENTATION"] = "semantic_segmentation";
9458
- InferenceType2["SEGMENTATION"] = "segmentation";
9459
- return InferenceType2;
9460
- })(InferenceType || {});
9461
- var MotionModel = /* @__PURE__ */ ((MotionModel2) => {
9462
- MotionModel2["RANDOM_WALK"] = "random_walk";
9463
- MotionModel2["CONSTANT_VELOCITY"] = "constant_velocity";
9464
- MotionModel2["CONSTANT_ACCELERATION"] = "constant_acceleration";
9465
- return MotionModel2;
9466
- })(MotionModel || {});
9467
- var ContourType = /* @__PURE__ */ ((ContourType2) => {
9468
- ContourType2["ALL_PIXELS"] = "all_pixels";
9469
- ContourType2["POLYGON"] = "polygon";
9470
- ContourType2["CONVEX_HULL"] = "convex_hull";
9471
- ContourType2["HOUGH_CIRCLES"] = "hough_circles";
9472
- ContourType2["CIRCLE"] = "circle";
9473
- ContourType2["TRIANGLE"] = "triangle";
9474
- ContourType2["RECTANGLE"] = "rectangle";
9475
- return ContourType2;
9476
- })(ContourType || {});
9477
- function validatePop(pop) {
9478
- if (pop.depthMap !== void 0) {
9479
- if (pop.depthMap.ability === void 0 === (pop.depthMap.abilityUuid === void 0)) {
9480
- throw new Error("depthMap requires exactly one of ability or abilityUuid");
9481
- }
9482
- }
9483
- if (pop.defaults?.camera !== void 0) {
9484
- validateCamera(pop.defaults.camera);
9485
- }
9486
- validateForwards(pop.components);
9487
- }
9488
- function popSelects(pop) {
9489
- const selects = (components) => (components ?? []).some((component) => isSelectOperator(component.forward?.operator) || selects(component.forward?.targets));
9490
- return selects(pop?.components);
9491
- }
9492
- function validateForwards(components) {
9493
- for (const component of components ?? []) {
9494
- const operator = component.forward?.operator;
9495
- if (operator !== void 0) {
9496
- validateSelectOperator(operator);
9497
- }
9498
- validateForwards(component.forward?.targets);
9499
- }
9500
- }
9501
- function validateSelectOperator(operator) {
9502
- if (!isSelectOperator(operator)) {
9503
- if (operator.select !== void 0) {
9504
- throw new Error("select is only valid with the select_crop or select_full operator");
9505
- }
9506
- return;
9507
- }
9508
- const select = operator.select;
9509
- if (select === void 0) {
9510
- throw new Error(`${operator.type} requires a select block`);
9511
- }
9512
- if (select.mode !== void 0 && select.mode !== "most_relevant" /* MOST_RELEVANT */) {
9513
- throw new Error(`select mode ${select.mode} is not supported, the only mode is ${"most_relevant" /* MOST_RELEVANT */}`);
9514
- }
9515
- if (select.relevancyModel !== void 0 && select.relevancyModelUuid !== void 0) {
9516
- throw new Error("select can only have one of relevancyModelUuid or relevancyModel");
9517
- }
9518
- if (select.minTrackLengthSeconds !== void 0 && select.minTrackLengthSeconds < 0) {
9519
- throw new Error("select minTrackLengthSeconds cannot be negative");
9520
- }
9521
- if (select.intervalSeconds !== void 0 && select.intervalSeconds <= 0) {
9522
- throw new Error("select intervalSeconds must be positive");
9523
- }
9524
- if (operator.crop !== void 0) {
9525
- if (operator.type !== "select_crop" /* SELECT_CROP */) {
9526
- throw new Error(`crop options are only valid with select_crop, not ${operator.type}`);
9527
- }
9528
- if (operator.crop.maxItems !== void 0) {
9529
- throw new Error("select_crop forwards one detection per selection, maxItems does not apply");
9530
- }
9531
- }
9532
- }
9533
-
9534
10020
  // worker/webrtc_whip.ts
9535
10021
  var import_uuid2 = require("uuid");
9536
10022
 
@@ -10261,7 +10747,7 @@ var UploadGroupJob = class extends AbstractJob {
10261
10747
  parts.push({ name: "camera", contentType: "application/json", content: JSON.stringify(this._params.camera) });
10262
10748
  }
10263
10749
  for (const source of this._sources) {
10264
- parts.push({ name: "file", contentType: source.mimeType, content: source.stream });
10750
+ parts.push({ name: "file", filename: "blob", contentType: source.mimeType, content: source.stream });
10265
10751
  }
10266
10752
  const boundary = randomMultipartBoundary();
10267
10753
  const headers = {
@@ -10455,1310 +10941,1671 @@ if ("document" in globalThis && "implementation" in globalThis.document) {
10455
10941
  const mime = require_mime_types();
10456
10942
  const filehandle = require("fs/promises");
10457
10943
  const fd = await filehandle.open(source.path, "r");
10944
+ const size = (await fd.stat()).size;
10458
10945
  const stream = fd.createReadStream();
10459
10946
  const iterator = nodeStreamToIterator(stream);
10460
10947
  const webStream = iteratorToStream(iterator);
10461
- const mimeType = source.mimeType ? source.mimeType : mime.lookup(source.path) || "image/*";
10462
- return {
10463
- stream: webStream,
10464
- mimeType
10465
- };
10466
- };
10467
- }
10468
- var iteratorToStream2;
10469
-
10470
- // shims/http_client.ts
10471
- var createHttpClient;
10472
- function logLineFromRequest(input, init) {
10473
- const url = input instanceof Request ? input.url : input;
10474
- const method = input instanceof Request ? input.method : init?.method || (init?.body !== void 0 ? "POST" : "GET");
10475
- return `${method} ${url}`;
10476
- }
10477
- if ("document" in globalThis && "implementation" in globalThis.document) {
10478
- class BrowserHttpClient {
10479
- constructor(logger) {
10480
- this._logger = logger;
10481
- }
10482
- async fetch(input, init) {
10483
- const logLine = logLineFromRequest(input, init);
10484
- this._logger.debug(`Browser fetch() BEFORE ${logLine}`);
10485
- const response = await fetch(input, init);
10486
- this._logger.debug(`AFTER ${logLine} -> ${response.status}`);
10487
- return response;
10488
- }
10489
- async close() {
10490
- }
10491
- isFullDuplex() {
10492
- return false;
10493
- }
10494
- }
10495
- createHttpClient = async (logger) => {
10496
- return new BrowserHttpClient(logger);
10497
- };
10498
- } else {
10499
- class NodeHttpClient {
10500
- constructor(agent, fetchFn, logger) {
10501
- this._agent = agent;
10502
- this._fetch = fetchFn;
10503
- this._logger = logger;
10504
- }
10505
- async fetch(input, init) {
10506
- init || (init = {});
10507
- init.dispatcher = this._agent;
10508
- const logLine = logLineFromRequest(input, init);
10509
- this._logger.debug(`Node fetch() BEFORE ${logLine}`);
10510
- const response = await this._fetch(input, init);
10511
- this._logger.debug(`AFTER ${logLine} -> ${response.status}`);
10512
- return response;
10513
- }
10514
- async close() {
10515
- return this._agent.close();
10516
- }
10517
- isFullDuplex() {
10518
- return false;
10519
- }
10520
- }
10521
- createHttpClient = async (logger) => {
10522
- const undici = require("undici");
10523
- const agent = new undici.Agent({
10524
- keepAliveTimeout: 1e4,
10525
- connections: 5,
10526
- // two parallel streaming requests to the same host seem to deadlock with pipelining > 0
10527
- pipelining: 0
10528
- });
10529
- logger.debug("Using node fetch with undici agent as http client");
10530
- return new NodeHttpClient(agent, undici.fetch, logger);
10948
+ const mimeType = source.mimeType ? source.mimeType : mime.lookup(source.path) || "image/*";
10949
+ return {
10950
+ stream: webStream,
10951
+ mimeType,
10952
+ size
10953
+ };
10531
10954
  };
10532
10955
  }
10956
+ var iteratorToStream2;
10533
10957
 
10534
- // semaphore.ts
10535
- var Semaphore = class {
10536
- /**
10537
- * Creates a semaphore.
10538
- * @param permits The number of permits, i.e. strands of execution being allowed
10539
- * to run in parallel.
10540
- * This number can be initialized with a negative integer.
10541
- */
10542
- constructor(permits) {
10543
- this.promiseResolverQueue = [];
10544
- this.permits = permits;
10545
- }
10546
- /**
10547
- * Returns the number of available permits.
10548
- * @returns The number of available permits.
10549
- */
10550
- getPermits() {
10551
- return this.permits;
10552
- }
10553
- /**
10554
- * Returns a promise used to wait for a permit to become available. This method should be awaited on.
10555
- * @returns A promise that gets resolved when execution is allowed to proceed.
10556
- */
10557
- async wait() {
10558
- if (this.permits > 0) {
10559
- this.permits -= 1;
10560
- return Promise.resolve(true);
10561
- }
10562
- return new Promise((resolver) => this.promiseResolverQueue.push(resolver));
10958
+ // worker/worker_options.ts
10959
+ var TransientPopId = /* @__PURE__ */ ((TransientPopId2) => {
10960
+ TransientPopId2["Transient"] = "transient";
10961
+ return TransientPopId2;
10962
+ })(TransientPopId || {});
10963
+
10964
+ // worker/session_backend.ts
10965
+ var _SessionBackend = class _SessionBackend {
10966
+ constructor(ctx) {
10967
+ this.ctx = ctx;
10968
+ this._baseUrl = null;
10969
+ this._pipelineId = null;
10970
+ this._popName = null;
10971
+ this._sessionAccessToken = null;
10972
+ this._sessionAccessTokenValidUntil = null;
10973
+ // The account the connected compute session runs under, as compute-api reports it.
10974
+ this._accountId = void 0;
10975
+ this._pipeline = null;
10976
+ this._pipelineCreatePromise = null;
10563
10977
  }
10564
- /**
10565
- * Alias for {@linkcode Semaphore.wait}.
10566
- * @returns A promise that gets resolved when execution is allowed to proceed.
10567
- */
10568
- async acquire() {
10569
- return this.wait();
10978
+ async disconnect(closeEndpoint) {
10979
+ await this.deleteOwnPipeline();
10980
+ await closeEndpoint();
10570
10981
  }
10571
- /**
10572
- * Same as {@linkcode Semaphore.wait} except the promise returned gets resolved with false if no
10573
- * permit becomes available in time.
10574
- * @param milliseconds The time spent waiting before the wait is aborted. This is a lower bound,
10575
- * you shouldn't rely on it being precise.
10576
- * @returns A promise that gets resolved to true when execution is allowed to proceed or
10577
- * false if the time given elapses before a permit becomes available.
10578
- */
10579
- async waitFor(milliseconds) {
10580
- if (this.permits > 0) {
10581
- this.permits -= 1;
10582
- return Promise.resolve(true);
10583
- }
10584
- let resolver = (_) => void 0;
10585
- const promise = new Promise((r) => {
10586
- resolver = r;
10587
- });
10588
- this.promiseResolverQueue.push(resolver);
10589
- setTimeout(() => {
10590
- const index = this.promiseResolverQueue.indexOf(resolver);
10591
- if (index !== -1) {
10592
- this.promiseResolverQueue.splice(index, 1);
10593
- } else {
10594
- console.warn(`Semaphore.waitFor couldn't find its promise resolver in the queue`);
10982
+ async deleteOwnPipeline() {
10983
+ const client = this.ctx.client();
10984
+ if (this.ctx.options.popId == "transient" /* Transient */ && this._pipelineId && client) {
10985
+ const deleteUrl = `${this._baseUrl}/pipelines/${this._pipelineId}`;
10986
+ const headers = {
10987
+ Authorization: await this.authorizationHeader()
10988
+ };
10989
+ let response = await client.fetch(deleteUrl, {
10990
+ method: "DELETE",
10991
+ headers
10992
+ });
10993
+ if (response?.status != 204) {
10994
+ const message = await response?.text();
10995
+ this.ctx.logger.info(`stopping pipeline failed, status ${response?.status}: ${message}`);
10595
10996
  }
10596
- resolver(false);
10597
- }, milliseconds);
10598
- return promise;
10599
- }
10600
- /**
10601
- * Synchronous function that tries to acquire a permit and returns true if successful, false otherwise.
10602
- * @returns Whether a permit could be acquired.
10603
- */
10604
- tryAcquire() {
10605
- if (this.permits > 0) {
10606
- this.permits -= 1;
10607
- return true;
10608
10997
  }
10609
- return false;
10610
10998
  }
10611
- /**
10612
- * Acquires all permits that are currently available and returns the number of acquired permits.
10613
- * @returns Number of acquired permits.
10614
- */
10615
- drainPermits() {
10616
- if (this.permits > 0) {
10617
- const permitCount = this.permits;
10618
- this.permits = 0;
10619
- return permitCount;
10999
+ onStatus(statusCode) {
11000
+ if (statusCode == 404) {
11001
+ this._pipelineId = null;
11002
+ this._baseUrl = null;
10620
11003
  }
10621
- return 0;
10622
11004
  }
10623
- /**
10624
- * Increases the number of permits by one. If there are other functions waiting, one of them will
10625
- * continue to execute in a future iteration of the event loop.
10626
- */
10627
- signal() {
10628
- this.permits += 1;
10629
- if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
10630
- console.warn("Semaphore.permits should never be > 0 when there is someone waiting.");
10631
- } else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
10632
- this.permits -= 1;
10633
- const nextResolver = this.promiseResolverQueue.shift();
10634
- if (nextResolver) {
10635
- nextResolver(true);
11005
+ async session() {
11006
+ let session = null;
11007
+ if (this._baseUrl == null) {
11008
+ await this.reconnect();
11009
+ if (this._baseUrl == null) {
11010
+ return Promise.reject("endpoint not connected");
10636
11011
  }
10637
11012
  }
11013
+ if (this._sessionAccessToken) {
11014
+ session = {
11015
+ eyepopUrl: this.ctx.options.eyepopUrl,
11016
+ accessToken: this._sessionAccessToken,
11017
+ validUntil: this._sessionAccessTokenValidUntil || Number.MAX_VALUE
11018
+ };
11019
+ } else {
11020
+ session = await this.ctx.credentialSession();
11021
+ }
11022
+ return {
11023
+ eyepopUrl: session.eyepopUrl,
11024
+ accessToken: session.accessToken,
11025
+ validUntil: session.validUntil,
11026
+ popId: this.ctx.options.popId,
11027
+ accountId: this._accountId ?? this.ctx.options.accountId,
11028
+ baseUrl: this._baseUrl,
11029
+ pipelineId: this._pipelineId || void 0,
11030
+ isPipelineMode: false,
11031
+ authenticationHeaders: () => this._authenticationHeaders(session)
11032
+ };
10638
11033
  }
10639
- /**
10640
- * Alias for {@linkcode Semaphore.signal}.
10641
- */
10642
- release() {
10643
- this.signal();
11034
+ popName() {
11035
+ return this._popName;
11036
+ }
11037
+ pop() {
11038
+ if (this._pipeline) {
11039
+ return this._pipeline.pop || null;
11040
+ }
11041
+ return this.ctx.options.pop || null;
10644
11042
  }
10645
11043
  /**
10646
- * Schedules func to be called once a permit becomes available.
10647
- * Returns a promise that resolves to the return value of func.
10648
- * @typeparam T The return type of func.
10649
- * @param func The function to be executed.
10650
- * @return A promise that gets resolved with the return value of the function.
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.
10651
11047
  */
10652
- async execute(func) {
10653
- await this.wait();
10654
- try {
10655
- return await func();
10656
- } finally {
10657
- this.signal();
10658
- }
11048
+ predictionVersion() {
11049
+ return popSelects(this.pop()) ? 3 /* V3 */ : DEFAULT_PREDICTION_VERSION;
10659
11050
  }
10660
- };
10661
-
10662
- // endpoint.ts
10663
- var import_pino = require("pino");
10664
-
10665
- // shims/browser_session.ts
10666
- var import_auth0_spa_js = require("@auth0/auth0-spa-js");
10667
- var authenticateBrowserSession;
10668
- if ("document" in globalThis && "implementation" in globalThis.document) {
10669
- authenticateBrowserSession = async (auth0, options) => {
10670
- if (options.eyepopUrl == null) {
10671
- return Promise.reject("options.eyepopUrl cannot be null");
10672
- }
10673
- const auth0ClientOptions = {
10674
- clientId: auth0.clientId,
10675
- domain: auth0.domain,
10676
- authorizationParams: {
10677
- scope: auth0.scope,
10678
- audience: auth0.audience
10679
- }
10680
- };
10681
- const auth0Client = await (0, import_auth0_spa_js.createAuth0Client)(auth0ClientOptions);
10682
- if (!await auth0Client.isAuthenticated()) {
10683
- await auth0Client.loginWithPopup();
10684
- }
10685
- const accessToken = await auth0Client.getTokenSilently();
10686
- if (!accessToken) {
10687
- return Promise.reject("auth0 login failed");
11051
+ async changePop(pop) {
11052
+ if (this.ctx.options.popId == "transient" /* Transient */) {
11053
+ await this.changeTransientPop(pop);
11054
+ return;
10688
11055
  }
10689
- let session;
10690
- const workerOptions = options;
10691
- if (typeof workerOptions.popId != "undefined") {
10692
- const workerSession = {
10693
- eyepopUrl: options.eyepopUrl,
10694
- accessToken,
10695
- validUntil: Date.now() + 60 * 60 * 1e3,
10696
- popId: workerOptions.popId,
10697
- baseUrl: void 0,
10698
- pipelineId: void 0,
10699
- authenticationHeaders: () => {
10700
- return { Authorization: `Bearer ${accessToken}` };
10701
- }
10702
- };
10703
- session = workerSession;
10704
- } else {
10705
- session = {
10706
- eyepopUrl: options.eyepopUrl,
10707
- accessToken,
10708
- validUntil: Date.now() + 60 * 60 * 1e3
10709
- };
11056
+ const client = this.ctx.client();
11057
+ const baseUrl = this._baseUrl;
11058
+ if (!baseUrl || !client) {
11059
+ return Promise.reject("endpoint not connected, use connect()");
10710
11060
  }
10711
- return session;
10712
- };
10713
- } else {
10714
- authenticateBrowserSession = (_auth0, _options) => {
10715
- return Promise.reject("auth0 login not supported server-side");
10716
- };
10717
- }
10718
-
10719
- // endpoint.ts
10720
- var Endpoint = class {
10721
- constructor(options) {
10722
- this._options = options;
10723
- this._token = null;
10724
- this._expire_token_time = null;
10725
- this._client = null;
10726
- this._statusRetryHandlers = /* @__PURE__ */ new Map();
10727
- this._statusRetryHandlers.set(401, this.statusHandler401);
10728
- this._limit = new Semaphore(this._options.jobQueueLength ?? 1024);
10729
- this._state = "Idle" /* Idle */;
10730
- this._message = null;
10731
- this._stateChangeHandler = null;
10732
- let rootLogger;
10733
- if (options.logger) {
10734
- rootLogger = options.logger;
11061
+ let response = await this.ctx.fetchWithRetry(async () => {
11062
+ const session = await this.session();
11063
+ let headers = {
11064
+ "Content-Type": "application/json",
11065
+ ...this._authenticationHeaders(session)
11066
+ };
11067
+ const patch_url = `${session.baseUrl}/pipelines/${session.pipelineId}/pop`;
11068
+ return await client.fetch(patch_url, {
11069
+ method: "PATCH",
11070
+ body: JSON.stringify(pop),
11071
+ headers
11072
+ });
11073
+ });
11074
+ if (response.status != 204) {
11075
+ const message = await response.text();
11076
+ throw new Error(`Unexpected status ${response.status}: ${message}`);
11077
+ }
11078
+ this.ctx.options.pop = pop;
11079
+ if (this._pipeline) {
11080
+ this._pipeline.pop = pop;
10735
11081
  } else {
10736
- rootLogger = (0, import_pino.pino)();
11082
+ this._pipeline = {
11083
+ id: this._pipelineId || "unknown",
11084
+ state: "idle",
11085
+ startTime: (/* @__PURE__ */ new Date()).toString(),
11086
+ pop
11087
+ };
10737
11088
  }
10738
- this._logger = rootLogger.child({ module: "eyepop" });
10739
- this._requestLogger = this._logger.child({ module: "requests" });
10740
11089
  }
10741
- setStatusRetryHandlers(statusRetryHandlers = void 0) {
10742
- this._statusRetryHandlers = new Map(statusRetryHandlers);
10743
- this._statusRetryHandlers.set(401, this.statusHandler401);
11090
+ _authenticationHeaders(session) {
11091
+ return resolveAuth(this.ctx.options)?.isLocal !== void 0 ? {} : { Authorization: `Bearer ${session.accessToken}` };
10744
11092
  }
10745
- eyepopUrl() {
10746
- if (this._options.eyepopUrl) {
10747
- return this._options.eyepopUrl.replace(/\/+$/, "");
10748
- } else {
10749
- return "https://compute.eyepop.ai";
11093
+ async submit(work) {
11094
+ switch (work.kind) {
11095
+ case "source":
11096
+ return await this.process(work.source, work.params);
11097
+ case "uploadGroup":
11098
+ return await this.uploadGroup(work.paths, work.params);
11099
+ case "uploadStreamGroup":
11100
+ return await this.uploadStreamGroup(work.streams, work.mimeTypes, work.params);
11101
+ case "loadFromGroup":
11102
+ return await this.loadFromGroup(work.urls, work.params);
10750
11103
  }
10751
11104
  }
10752
- // TODO: deprecated Web API endpoint needed for POP_ID support
10753
- eyepopWebApiUrl() {
10754
- if (this._options.eyepopUrl) {
10755
- return this._options.eyepopUrl.replace(/\/+$/, "").replace(/compute\./, "web-api.");
11105
+ async process(source, params) {
11106
+ if (source.file !== void 0) {
11107
+ return this.uploadFile(source, params);
11108
+ } else if (source.stream !== void 0) {
11109
+ return this.uploadStream(source, params);
11110
+ } else if (source.path !== void 0) {
11111
+ return this.uploadPath(source, params);
11112
+ } else if (source.url !== void 0) {
11113
+ return this.loadFrom(source, params);
11114
+ } else if (source.assetUuid !== void 0) {
11115
+ return this.loadFromAssetUuid(source, params);
11116
+ } else if (source.mediaStream !== void 0) {
11117
+ return this.loadMediaStream(source, params);
10756
11118
  } else {
10757
- return "https://api.eyepop.ai";
11119
+ return Promise.reject("unknown source type");
10758
11120
  }
10759
11121
  }
10760
- pendingJobs() {
10761
- return (this._options.jobQueueLength ?? 1024) - this._limit.getPermits();
10762
- }
10763
- state() {
10764
- return this._state;
10765
- }
10766
- lastMessage() {
10767
- return this._message;
10768
- }
10769
- onStateChanged(handler) {
10770
- this._stateChangeHandler = handler;
10771
- return this;
10772
- }
10773
- updateState(newState = void 0, message = void 0) {
10774
- const fromState = this._state;
10775
- if (newState == void 0) {
10776
- if (this._limit.getPermits() == this._options.jobQueueLength) {
10777
- newState = "Idle" /* Idle */;
10778
- } else {
10779
- newState = "Busy" /* Busy */;
11122
+ start(job) {
11123
+ return job.start(
11124
+ () => {
11125
+ this.ctx.jobDone();
11126
+ },
11127
+ (statusCode) => {
11128
+ if (statusCode == 404) {
11129
+ this.onStatus(statusCode);
11130
+ } else if (statusCode == 401) {
11131
+ this.ctx.forgetCredential();
11132
+ }
10780
11133
  }
10781
- }
10782
- this._state = newState;
10783
- this._message = message ? message : null;
10784
- if (this._stateChangeHandler) {
10785
- this._stateChangeHandler(fromState, this._state);
10786
- }
11134
+ );
10787
11135
  }
10788
- async connect() {
10789
- if (this._client) {
10790
- this._logger.warn("endpoint already connected");
10791
- return this;
10792
- }
10793
- if (!this._options.eyepopUrl) {
10794
- return Promise.reject("option eyepopUrl or environment variable EYEPOP_URL is required");
11136
+ async uploadGroup(paths, params) {
11137
+ const client = this.ctx.client();
11138
+ const limit = this.ctx.limit();
11139
+ if (!this._baseUrl || !this._pipelineId || !client) {
11140
+ throw new Error("endpoint not connected, use connect()");
10795
11141
  }
10796
- const auth = resolveAuth(this._options);
10797
- if (auth === void 0) {
10798
- return Promise.reject("cannot connect without defined auth option");
11142
+ await limit.acquire();
11143
+ try {
11144
+ this.ctx.updateState();
11145
+ const sources = await Promise.all(
11146
+ paths.map(async (path2) => {
11147
+ const streamSource = await this.resolvePath({ path: path2 });
11148
+ return { stream: streamSource.stream, mimeType: streamSource.mimeType };
11149
+ })
11150
+ );
11151
+ return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11152
+ } catch (e) {
11153
+ limit.release();
11154
+ throw e;
10799
11155
  }
10800
- if (auth.session !== void 0) {
10801
- this._token = auth.session.accessToken;
10802
- this._expire_token_time = auth.session.validUntil / 1e3;
10803
- } else if (auth.oAuth2 !== void 0) {
10804
- } else if (accessTokenCredential(auth) !== void 0) {
10805
- } else if (apiKeyCredential(auth) !== void 0) {
10806
- } else if (auth.isLocal !== void 0) {
10807
- } else {
10808
- return Promise.reject("option apiKey/accessToken or environment variable EYEPOP_API_KEY is required");
11156
+ }
11157
+ async uploadStreamGroup(streams, mimeTypes, params) {
11158
+ const client = this.ctx.client();
11159
+ const limit = this.ctx.limit();
11160
+ if (!this._baseUrl || !this._pipelineId || !client) {
11161
+ throw new Error("endpoint not connected, use connect()");
10809
11162
  }
10810
- if (this._options.platformSupport?.createHttpClient !== void 0) {
10811
- this._client = await this._options.platformSupport.createHttpClient(this._requestLogger);
10812
- } else {
10813
- this._client = await createHttpClient(this._requestLogger);
11163
+ await limit.acquire();
11164
+ try {
11165
+ this.ctx.updateState();
11166
+ const sources = streams.map((stream, i) => ({
11167
+ stream,
11168
+ mimeType: mimeTypes?.[i] ?? null
11169
+ }));
11170
+ return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11171
+ } catch (e) {
11172
+ limit.release();
11173
+ throw e;
10814
11174
  }
10815
- return await this.reconnect();
10816
11175
  }
10817
- async disconnect(wait = true) {
10818
- if (wait && this._limit) {
10819
- for (let j = 0; j < this._options.jobQueueLength; j++) {
10820
- await this._limit.acquire();
10821
- }
11176
+ async loadFromGroup(urls, params) {
11177
+ const client = this.ctx.client();
11178
+ const limit = this.ctx.limit();
11179
+ if (!this._baseUrl || !this._pipelineId || !client) {
11180
+ throw new Error("endpoint not connected, use connect()");
10822
11181
  }
10823
- this._limit = new Semaphore(this._options.jobQueueLength ?? 1024);
10824
- if (this._client) {
10825
- await this._client.close();
10826
- this._client = null;
11182
+ await limit.acquire();
11183
+ try {
11184
+ this.ctx.updateState();
11185
+ return this.start(new LoadFromGroupJob(urls, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11186
+ } catch (e) {
11187
+ limit.release();
11188
+ throw e;
10827
11189
  }
10828
11190
  }
10829
- async session() {
10830
- await this.currentAccessToken();
10831
- if (this._token == null || this._expire_token_time == null) {
10832
- return Promise.reject("endpoint not connected");
11191
+ async resolvePath(source) {
11192
+ if (this.ctx.options.platformSupport?.resolvePath) {
11193
+ return await this.ctx.options.platformSupport.resolvePath(source, this.ctx.logger);
10833
11194
  }
10834
- return {
10835
- eyepopUrl: this._options.eyepopUrl,
10836
- accessToken: this._token,
10837
- validUntil: this._expire_token_time * 1e3
10838
- };
10839
- }
10840
- statusHandler401(_statusCode) {
10841
- this._token = null;
10842
- this._expire_token_time = null;
11195
+ return await resolvePath(source, this.ctx.logger);
10843
11196
  }
10844
- async fetchWithRetry(fetcher, retries = 1) {
10845
- while (true) {
10846
- try {
10847
- const response = await fetcher();
10848
- if (response.ok) {
10849
- return response;
10850
- } else {
10851
- const handler = this._statusRetryHandlers.get(response.status);
10852
- if (handler != null) {
10853
- handler(response.status);
10854
- if (retries--) {
10855
- this._requestLogger.info("received %d response, retry", response.status);
10856
- } else {
10857
- return response;
10858
- }
10859
- } else {
10860
- return response;
10861
- }
10862
- }
10863
- } catch (error) {
10864
- const handler = this._statusRetryHandlers.get(404);
10865
- if (handler != null) {
10866
- handler(error);
10867
- if (retries--) {
10868
- this._requestLogger.info("error %s, retry", error);
10869
- } else {
10870
- return Promise.reject(`error ${error}`);
10871
- }
10872
- } else {
10873
- return Promise.reject(`error ${error}`);
10874
- }
11197
+ /** Start a job once the transient pipeline runs and a queue permit is held; the permit is released if it cannot start. */
11198
+ async startConnected(makeJob, rejectsWithText = false) {
11199
+ await this.ensureTransientPipelineStarted();
11200
+ const client = this.ctx.client();
11201
+ const limit = this.ctx.limit();
11202
+ if (!this._baseUrl || !this._pipelineId || !client) {
11203
+ if (rejectsWithText) {
11204
+ return Promise.reject("endpoint not connected, use connect()");
10875
11205
  }
11206
+ throw new Error("endpoint not connected, use connect()");
11207
+ }
11208
+ await limit.acquire();
11209
+ try {
11210
+ this.ctx.updateState();
11211
+ return this.start(await makeJob(client));
11212
+ } catch (e) {
11213
+ limit.release();
11214
+ throw e;
10876
11215
  }
10877
11216
  }
10878
- async reconnect() {
10879
- throw new Error("reconnect() not implemented");
11217
+ async uploadFile(source, params) {
11218
+ return await this.startConnected(
11219
+ async (client) => new UploadJob(
11220
+ source.file,
11221
+ source.file.type,
11222
+ source.file.size,
11223
+ source.videoMode,
11224
+ params,
11225
+ async () => this.session(),
11226
+ client,
11227
+ this.ctx.requestLogger,
11228
+ void 0,
11229
+ this.predictionVersion()
11230
+ ),
11231
+ true
11232
+ );
10880
11233
  }
10881
- async authorizationHeader() {
10882
- if (resolveAuth(this._options)?.isLocal !== void 0) {
10883
- return `Implicit`;
10884
- }
10885
- return `Bearer ${await this.currentAccessToken()}`;
11234
+ async uploadStream(source, params) {
11235
+ 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()),
11237
+ true
11238
+ );
10886
11239
  }
10887
- async currentAccessToken() {
10888
- const now = Date.now() / 1e3;
10889
- if (!this._token || this._expire_token_time < now) {
10890
- this.updateState("Authenticating" /* Authenticating */);
10891
- try {
10892
- const auth = resolveAuth(this._options);
10893
- if (!this._client) {
10894
- return Promise.reject("endpoint not connected");
10895
- } else if (auth?.isLocal !== void 0) {
10896
- this._token = "implicit";
10897
- this._expire_token_time = Number.MAX_VALUE;
10898
- } else if (auth?.session !== void 0) {
10899
- return Promise.reject("temporary access token expired");
10900
- } else if (auth?.oAuth2 !== void 0 && this._options.eyepopUrl) {
10901
- const session = await authenticateBrowserSession(auth.oAuth2, this._options);
10902
- this._token = session.accessToken;
10903
- this._expire_token_time = session.validUntil / 1e3;
10904
- } else if (accessTokenCredential(auth) !== void 0) {
10905
- this._token = accessTokenCredential(auth);
10906
- this._expire_token_time = Number.MAX_VALUE;
10907
- } else if (this._options.eyepopUrl) {
10908
- const credential = apiKeyCredential(auth);
10909
- if (credential === void 0) {
10910
- return Promise.reject("no valid auth option");
10911
- }
10912
- const headers = { Authorization: `Bearer ${credential}` };
10913
- const post_url = `${this.eyepopUrl()}/v1/auth/authenticate`;
10914
- const response = await this._client.fetch(post_url, {
10915
- method: "POST",
10916
- headers
10917
- });
10918
- if (response.status != 200) {
10919
- const message = await response.text();
10920
- return Promise.reject(`Unexpected status ${response.status}: ${message}`);
10921
- }
10922
- const data = await response.json();
10923
- const token = data;
10924
- this._token = token.access_token;
10925
- this._expire_token_time = now + token.expires_in - 60;
10926
- } else {
10927
- return Promise.reject("no valid auth option");
11240
+ async uploadPath(source, params) {
11241
+ return await this.startConnected(async (client) => {
11242
+ const streamSource = await this.resolvePath(source);
11243
+ let firstStreamPending = true;
11244
+ const uploadBodyFactory = async () => {
11245
+ if (firstStreamPending) {
11246
+ firstStreamPending = false;
11247
+ return streamSource.stream;
10928
11248
  }
10929
- } finally {
10930
- this.updateState();
10931
- }
11249
+ return (await this.resolvePath(source)).stream;
11250
+ };
11251
+ return new UploadJob(
11252
+ streamSource.stream,
11253
+ streamSource.mimeType,
11254
+ streamSource.size,
11255
+ source.videoMode,
11256
+ params,
11257
+ async () => this.session(),
11258
+ client,
11259
+ this.ctx.requestLogger,
11260
+ uploadBodyFactory,
11261
+ this.predictionVersion()
11262
+ );
11263
+ });
11264
+ }
11265
+ async loadFrom(source, params) {
11266
+ if (source.url.startsWith("file://")) {
11267
+ return await this.uploadPath({ path: source.url.substring("file://".length) }, params);
10932
11268
  }
10933
- this._logger.debug("using access token, valid for at least %d seconds", this._expire_token_time - now);
10934
- return this._token;
11269
+ return await this.startConnected(async (client) => new LoadFromJob(source.url, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
10935
11270
  }
10936
- };
10937
-
10938
- // worker/worker_options.ts
10939
- var TransientPopId = /* @__PURE__ */ ((TransientPopId2) => {
10940
- TransientPopId2["Transient"] = "transient";
10941
- return TransientPopId2;
10942
- })(TransientPopId || {});
10943
-
10944
- // worker/worker_endpoint.ts
10945
- var _WorkerEndpoint = class _WorkerEndpoint extends Endpoint {
10946
- constructor(options) {
10947
- super(options);
10948
- this.setStatusRetryHandlers(/* @__PURE__ */ new Map([[404, (statusCode) => this.statusHandler404(statusCode)]]));
10949
- this._baseUrl = null;
10950
- this._pipelineId = null;
10951
- this._popName = null;
10952
- this._sessionAccessToken = null;
10953
- this._sessionAccessTokenValidUntil = null;
10954
- this._accountId = void 0;
10955
- this._pipeline = null;
10956
- this._pipelineCreatePromise = null;
11271
+ 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()));
10957
11273
  }
10958
- options() {
10959
- return this._options;
11274
+ 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()));
10960
11276
  }
10961
- async disconnect(wait = true) {
10962
- if (this.options().popId == "transient" /* Transient */ && this._pipelineId) {
10963
- const deleteUrl = `${this._baseUrl}/pipelines/${this._pipelineId}`;
10964
- const headers = {
11277
+ async reconnect() {
11278
+ if (this.ctx.options.popId == "transient" /* Transient */) {
11279
+ await this.getComputeSession();
11280
+ if (this.ctx.options.pop) {
11281
+ this.setTransientPipelinePop();
11282
+ }
11283
+ } else if (this.ctx.options.sessionUuid) {
11284
+ await this.getComputeSession();
11285
+ } else {
11286
+ await this.startWebApiSessionWithPopId();
11287
+ }
11288
+ this.ctx.updateState();
11289
+ }
11290
+ async startWebApiSessionWithPopId() {
11291
+ const client = this.ctx.client();
11292
+ if (!client) {
11293
+ throw new Error("endpoint not initialized");
11294
+ }
11295
+ const config_url = `${this.ctx.eyepopWebApiUrl()}/pops/${this.ctx.options.popId}/config?auto_start=false`;
11296
+ const headers = {
11297
+ Authorization: await this.authorizationHeader()
11298
+ };
11299
+ this.ctx.updateState("FetchConfig" /* FetchConfig */);
11300
+ let response = await client.fetch(config_url, { headers });
11301
+ if (response.status == 401) {
11302
+ this.ctx.requestLogger.debug("GET %s: 401, about to retry with fresh access token", config_url);
11303
+ this.ctx.forgetCredential();
11304
+ const headers2 = {
10965
11305
  Authorization: await this.authorizationHeader()
10966
11306
  };
10967
- let response = await this._client?.fetch(deleteUrl, {
10968
- method: "DELETE",
10969
- headers
11307
+ this.ctx.updateState("FetchConfig" /* FetchConfig */);
11308
+ response = await client.fetch(config_url, {
11309
+ headers: headers2
10970
11310
  });
10971
- if (response?.status != 204) {
10972
- const message = await response?.text();
10973
- this._logger.info(`stopping pipeline failed, status ${response?.status}: ${message}`);
11311
+ }
11312
+ if (response.status != 200) {
11313
+ const message = await response.text();
11314
+ if (response.status == 429) {
11315
+ this.ctx.updateState("NotAvailable" /* NotAvailable */, message);
11316
+ } else {
11317
+ this.ctx.updateState("Error" /* Error */, message);
10974
11318
  }
11319
+ throw new Error(`Unexpected status ${response.status}: ${message}`);
10975
11320
  }
10976
- return super.disconnect(wait);
10977
- }
10978
- statusHandler404(_) {
10979
- this._pipelineId = null;
10980
- this._baseUrl = null;
10981
- }
10982
- async session() {
10983
- let session = null;
10984
- if (this._baseUrl == null) {
10985
- await this.reconnect();
10986
- if (this._baseUrl == null) {
10987
- return Promise.reject("endpoint not connected");
11321
+ let config = await response.json();
11322
+ if (!config.base_url && this.ctx.options.autoStart) {
11323
+ const auto_start_config_url = `${this.ctx.eyepopWebApiUrl()}/pops/${this.ctx.options.popId}/config?auto_start=true`;
11324
+ this.ctx.requestLogger.debug("pop was not running, trying to autostart with: %s", auto_start_config_url);
11325
+ const headers2 = {
11326
+ Authorization: await this.authorizationHeader()
11327
+ };
11328
+ response = await client.fetch(auto_start_config_url, {
11329
+ headers: headers2
11330
+ });
11331
+ if (response.status != 200) {
11332
+ this.ctx.updateState("Error" /* Error */);
11333
+ const message = await response.text();
11334
+ throw new Error(`Unexpected status ${response.status}: ${message}`);
10988
11335
  }
11336
+ config = await response.json();
10989
11337
  }
10990
- if (this._sessionAccessToken) {
10991
- session = {
10992
- eyepopUrl: this.options().eyepopUrl,
10993
- accessToken: this._sessionAccessToken,
10994
- validUntil: this._sessionAccessTokenValidUntil || Number.MAX_VALUE
11338
+ const baseUrl = new URL(config.base_url, this.ctx.eyepopUrl());
11339
+ this._baseUrl = baseUrl.toString();
11340
+ if (this._baseUrl) {
11341
+ this._baseUrl = this._baseUrl.replace(/\/+$/, "");
11342
+ this._pipelineId = config.pipeline_id;
11343
+ this._popName = config.name;
11344
+ if (!this._pipelineId || !this._baseUrl) {
11345
+ throw new Error("Pop not started");
11346
+ }
11347
+ const get_url = `${this._baseUrl}/pipelines/${this._pipelineId}`;
11348
+ const headers2 = {
11349
+ Authorization: await this.authorizationHeader()
10995
11350
  };
10996
- } else {
10997
- session = await super.session();
11351
+ let response2 = await client.fetch(get_url, {
11352
+ method: "GET",
11353
+ headers: headers2
11354
+ });
11355
+ if (response2.status > 200) {
11356
+ const message = await response2.text();
11357
+ throw new Error(`Unexpected status ${response2.status}: ${message}`);
11358
+ }
11359
+ this._pipeline = await response2.json();
11360
+ if (this.ctx.options.stopJobs) {
11361
+ const body = { sourceType: "NONE" };
11362
+ const headers3 = {
11363
+ Authorization: await this.authorizationHeader(),
11364
+ "Content-Type": "application/json"
11365
+ };
11366
+ const stop_url = `${this._baseUrl}/pipelines/${this._pipeline.id}/source?mode=preempt&processing=sync`;
11367
+ let response3 = await client.fetch(stop_url, {
11368
+ method: "PATCH",
11369
+ headers: headers3,
11370
+ body: JSON.stringify(body)
11371
+ });
11372
+ if (response3.status >= 300) {
11373
+ const message = await response3.text();
11374
+ throw new Error(`Unexpected status ${response3.status}: ${message}`);
11375
+ }
11376
+ }
10998
11377
  }
10999
- return {
11000
- eyepopUrl: session.eyepopUrl,
11001
- accessToken: session.accessToken,
11002
- validUntil: session.validUntil,
11003
- popId: this.options().popId,
11004
- accountId: this._accountId ?? this.options().accountId,
11005
- baseUrl: this._baseUrl,
11006
- pipelineId: this._pipelineId || void 0,
11007
- authenticationHeaders: () => this._authenticationHeaders(session)
11008
- };
11009
11378
  }
11010
- popName() {
11011
- return this._popName;
11379
+ sessionReadyTimeoutMs() {
11380
+ const seconds = this.ctx.options.sessionReadyTimeoutSeconds;
11381
+ return typeof seconds == "number" && Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : _SessionBackend.WAIT_FOR_IS_READY;
11012
11382
  }
11013
- pop() {
11014
- if (this._pipeline) {
11015
- return this._pipeline.pop || null;
11383
+ async getComputeSession() {
11384
+ const client = this.ctx.client();
11385
+ if (!client) {
11386
+ throw new Error("endpoint not initialized");
11016
11387
  }
11017
- return this.options().pop || null;
11018
- }
11019
- /**
11020
- * A select forward's results come only at V3. Only a Pop that selects asks
11021
- * for it, so a worker too old to know V3 - which also rejects that Pop - is
11022
- * never sent a version it would not accept.
11023
- */
11024
- predictionVersion() {
11025
- return popSelects(this.pop()) ? 3 /* V3 */ : DEFAULT_PREDICTION_VERSION;
11026
- }
11027
- async changePop(pop) {
11028
- validatePop(pop);
11029
- if (this.options().popId == "transient" /* Transient */) {
11030
- await this.changeTransientPop(pop);
11031
- return;
11388
+ let resolved;
11389
+ try {
11390
+ resolved = await new ComputeSessionClient({
11391
+ computeUrl: this.ctx.eyepopUrl(),
11392
+ httpClient: client,
11393
+ authorizationHeader: () => this.computeAuthorizationHeader(),
11394
+ sessionUuid: this.ctx.options.sessionUuid,
11395
+ sessionName: this.ctx.options.sessionName,
11396
+ accountId: this.ctx.options.accountId,
11397
+ pipelineImage: this.ctx.options.pipelineImage,
11398
+ pipelineVersion: this.ctx.options.pipelineVersion,
11399
+ pop: this.ctx.options.pop,
11400
+ readyTimeoutMs: this.sessionReadyTimeoutMs(),
11401
+ logger: this.ctx.logger,
11402
+ requestLogger: this.ctx.requestLogger
11403
+ }).resolve();
11404
+ } catch (error) {
11405
+ this.ctx.updateState("Error" /* Error */);
11406
+ throw error;
11032
11407
  }
11033
- const client = this._client;
11408
+ this._sessionAccessToken = resolved.accessToken;
11409
+ this._sessionAccessTokenValidUntil = resolved.accessTokenValidUntil;
11410
+ this._baseUrl = resolved.session.session_endpoint;
11411
+ this._accountId = resolved.session.account_uuid || void 0;
11412
+ this._pipeline = null;
11413
+ this._pipelineId = resolved.pipelineId;
11414
+ if (this.ctx.options.pop) {
11415
+ this.setTransientPipelinePop();
11416
+ }
11417
+ }
11418
+ async startTransientPipeline() {
11419
+ const client = this.ctx.client();
11034
11420
  const baseUrl = this._baseUrl;
11035
- if (!baseUrl || !client) {
11036
- return Promise.reject("endpoint not connected, use connect()");
11421
+ if (client == null || baseUrl == null) {
11422
+ throw new Error("endpoint not initialized");
11037
11423
  }
11038
- let response = await this.fetchWithRetry(async () => {
11039
- const session = await this.session();
11424
+ let pop;
11425
+ if (this.ctx.options.pop) {
11426
+ pop = this.ctx.options.pop;
11427
+ } else if (this._pipeline && this._pipeline.pop) {
11428
+ pop = this._pipeline.pop;
11429
+ } else {
11430
+ throw new Error("Cannot start a transient pipeline without a pop");
11431
+ }
11432
+ const body = {
11433
+ pop,
11434
+ source: {
11435
+ sourceType: "NONE"
11436
+ },
11437
+ idleTimeoutSeconds: 30,
11438
+ logging: ["out_meta"],
11439
+ videoOutput: "no_output"
11440
+ };
11441
+ const post_url = `${baseUrl.replace(/\/+$/, "")}/pipelines`;
11442
+ const response = await this.ctx.fetchWithRetry(async () => {
11040
11443
  let headers = {
11444
+ Accept: "application/json",
11041
11445
  "Content-Type": "application/json",
11042
- ...this._authenticationHeaders(session)
11446
+ Authorization: await this.authorizationHeader()
11043
11447
  };
11044
- const patch_url = `${session.baseUrl}/pipelines/${session.pipelineId}/pop`;
11045
- return await client.fetch(patch_url, {
11046
- method: "PATCH",
11047
- body: JSON.stringify(pop),
11448
+ return client.fetch(post_url, {
11449
+ method: "POST",
11450
+ body: JSON.stringify(body),
11048
11451
  headers
11049
11452
  });
11050
11453
  });
11051
- if (response.status != 204) {
11454
+ if (response.status != 200) {
11455
+ const message = await response.text();
11456
+ throw new Error(`Unexpected status ${response.status} starting transient pipeline: ${message.slice(0, 300)}`);
11457
+ }
11458
+ const result = await response.json();
11459
+ return result["id"];
11460
+ }
11461
+ async ensureTransientPipelineStarted() {
11462
+ if (this.ctx.options.popId != "transient" /* Transient */ || this._pipelineId) {
11463
+ return;
11464
+ }
11465
+ if (!this.ctx.options.pop) {
11466
+ return;
11467
+ }
11468
+ if (!this._baseUrl) {
11469
+ await this.reconnect();
11470
+ }
11471
+ if (this._pipelineId) {
11472
+ return;
11473
+ }
11474
+ if (this.ctx.options.pop) {
11475
+ await this.reconnect();
11476
+ if (!this._pipelineId) {
11477
+ throw new Error("Compute session did not provide a pipeline for the requested pop");
11478
+ }
11479
+ return;
11480
+ }
11481
+ if (!this._pipelineCreatePromise) {
11482
+ this._pipelineCreatePromise = this.startTransientPipeline();
11483
+ }
11484
+ try {
11485
+ this._pipelineId = await this._pipelineCreatePromise;
11486
+ this.setTransientPipelinePop();
11487
+ } finally {
11488
+ this._pipelineCreatePromise = null;
11489
+ }
11490
+ }
11491
+ async changeTransientPop(pop) {
11492
+ if (!this.ctx.client()) {
11493
+ throw new Error("endpoint not initialized");
11494
+ }
11495
+ const previousPop = this.ctx.options.pop;
11496
+ const previousBaseUrl = this._baseUrl;
11497
+ const previousSessionAccessToken = this._sessionAccessToken;
11498
+ const previousSessionAccessTokenValidUntil = this._sessionAccessTokenValidUntil;
11499
+ if (this._pipelineId) {
11500
+ await this.deleteTransientPipeline(this._pipelineId);
11501
+ this._pipelineId = null;
11502
+ }
11503
+ this._pipeline = null;
11504
+ try {
11505
+ this.ctx.options.pop = pop;
11506
+ await this.getComputeSession();
11507
+ } catch (error) {
11508
+ this.ctx.options.pop = previousPop;
11509
+ this._pipeline = null;
11510
+ this._pipelineId = null;
11511
+ this._baseUrl = previousPop ? null : previousBaseUrl;
11512
+ this._sessionAccessToken = previousSessionAccessToken;
11513
+ this._sessionAccessTokenValidUntil = previousSessionAccessTokenValidUntil;
11514
+ throw error;
11515
+ }
11516
+ if (!this._pipelineId) {
11517
+ throw new Error("Compute session did not provide a pipeline for the requested pop");
11518
+ }
11519
+ this.setTransientPipelinePop();
11520
+ }
11521
+ async deleteTransientPipeline(pipelineId) {
11522
+ const client = this.ctx.client();
11523
+ if (!client || !this._baseUrl) {
11524
+ return;
11525
+ }
11526
+ const deleteUrl = `${this._baseUrl}/pipelines/${pipelineId}`;
11527
+ const headers = {
11528
+ Authorization: await this.authorizationHeader()
11529
+ };
11530
+ let response = await client.fetch(deleteUrl, {
11531
+ method: "DELETE",
11532
+ headers
11533
+ });
11534
+ if (response.status >= 300) {
11052
11535
  const message = await response.text();
11053
- throw new Error(`Unexpected status ${response.status}: ${message}`);
11536
+ this.ctx.logger.info(`stopping pipeline failed, status ${response.status}: ${message}`);
11054
11537
  }
11055
- this.options().pop = pop;
11056
- if (this._pipeline) {
11057
- this._pipeline.pop = pop;
11058
- } else {
11538
+ }
11539
+ async authorizationHeader() {
11540
+ if (this._sessionAccessToken) {
11541
+ return `Bearer ${this._sessionAccessToken}`;
11542
+ }
11543
+ return this.ctx.credentialHeader();
11544
+ }
11545
+ async computeAuthorizationHeader() {
11546
+ const auth = resolveAuth(this.ctx.options);
11547
+ if (auth?.isLocal !== void 0) {
11548
+ return "Implicit";
11549
+ }
11550
+ const credential = apiKeyCredential(auth) ?? accessTokenCredential(auth);
11551
+ if (credential !== void 0) {
11552
+ return `Bearer ${credential}`;
11553
+ }
11554
+ return this.ctx.credentialHeader();
11555
+ }
11556
+ setTransientPipelinePop() {
11557
+ const pop = this.ctx.options.pop;
11558
+ if (pop && this._pipelineId) {
11059
11559
  this._pipeline = {
11060
- id: this._pipelineId || "unknown",
11560
+ id: this._pipelineId,
11061
11561
  state: "idle",
11062
11562
  startTime: (/* @__PURE__ */ new Date()).toString(),
11063
11563
  pop
11064
11564
  };
11065
11565
  }
11066
11566
  }
11067
- _authenticationHeaders(session) {
11068
- return resolveAuth(this._options)?.isLocal !== void 0 ? {} : { Authorization: `Bearer ${session.accessToken}` };
11567
+ };
11568
+ _SessionBackend.WAIT_FOR_IS_READY = 60 * 1e3;
11569
+ var SessionBackend = _SessionBackend;
11570
+
11571
+ // shims/websockets.ts
11572
+ var createWebSocket;
11573
+ var UpgradeError = class extends Error {
11574
+ constructor(message, status, retryAfter) {
11575
+ super(message);
11576
+ this.name = "UpgradeError";
11577
+ this.status = status;
11578
+ this.retryAfter = retryAfter;
11069
11579
  }
11070
- /**
11071
- * @deprecated Pop ids are only retained for backwards compatibility.
11072
- * Configure transient sessions with `WorkerOptions.pop`.
11073
- */
11074
- popId() {
11075
- return this.options().popId || null;
11580
+ };
11581
+ var openAuthorizedWebSocket;
11582
+ var isBrowser = "document" in globalThis && "implementation" in globalThis.document;
11583
+ if (isBrowser) {
11584
+ createWebSocket = (url, protocols) => {
11585
+ return new WebSocket(url, protocols);
11586
+ };
11587
+ openAuthorizedWebSocket = (url, protocol, token) => {
11588
+ const protocols = token === void 0 ? [protocol] : [protocol, `eyepop.bearer.${token}`];
11589
+ const ws = new WebSocket(url, protocols);
11590
+ ws.binaryType = "arraybuffer";
11591
+ return new Promise((resolve, reject) => {
11592
+ ws.onopen = () => resolve(ws);
11593
+ ws.onerror = () => reject(new UpgradeError(`could not open ${url}`));
11594
+ });
11595
+ };
11596
+ } else {
11597
+ createWebSocket = (url, protocols) => {
11598
+ const ws = require("ws");
11599
+ return new ws.WebSocket(url, protocols);
11600
+ };
11601
+ openAuthorizedWebSocket = (url, protocol, token) => {
11602
+ const ws = require("ws");
11603
+ const headers = token === void 0 ? {} : { Authorization: `Bearer ${token}` };
11604
+ const socket = new ws.WebSocket(url, [protocol], { headers, maxPayload: 0 });
11605
+ socket.binaryType = "arraybuffer";
11606
+ return new Promise((resolve, reject) => {
11607
+ socket.once("open", () => resolve(socket));
11608
+ socket.once("unexpected-response", (_request, response) => {
11609
+ const retryAfter = Number.parseFloat(response.headers?.["retry-after"]);
11610
+ reject(new UpgradeError(`the pipeline API answered the upgrade ${response.statusCode}`, response.statusCode, Number.isFinite(retryAfter) ? retryAfter : void 0));
11611
+ response.resume();
11612
+ socket.terminate();
11613
+ });
11614
+ socket.once("error", (error) => reject(new UpgradeError(`could not open ${url}: ${error.message}`)));
11615
+ });
11616
+ };
11617
+ }
11618
+
11619
+ // worker/pipeline_backend.ts
11620
+ var PIPELINE_PATH = "/api/v1/pipeline";
11621
+ var SUBPROTOCOL = "eyepop.v1";
11622
+ var MAX_OPEN_SOURCES = 256;
11623
+ var COMBINED_UPLOAD_MAX_BYTES = 8 * 1024 * 1024;
11624
+ var CHUNK = 1;
11625
+ var COMBINED = 2;
11626
+ var CHUNK_BYTES = 64 * 1024;
11627
+ var HEARTBEAT_MS = 20 * 1e3;
11628
+ var MAX_RETRY_MS = 30 * 1e3;
11629
+ var RECONNECT_AT_ONCE = /* @__PURE__ */ new Set(["1008 credential_expired", "1011 overloaded", "1001 shutting_down"]);
11630
+ var MAX_RECONNECT_DELAY_MS = 30 * 1e3;
11631
+ var PipelineConfigurationError = class extends Error {
11632
+ constructor(problem, message, options = []) {
11633
+ super(message);
11634
+ this.name = "PipelineConfigurationError";
11635
+ this.problem = problem;
11636
+ this.options = options;
11076
11637
  }
11077
- async process(param1, param2) {
11078
- if (param1.source !== void 0) {
11079
- return await this.processV2(param1);
11080
- } else {
11081
- return await this.processV1(param1, param2);
11082
- }
11638
+ };
11639
+ var PipelineSocketClosedError = class extends Error {
11640
+ constructor(code, reason) {
11641
+ super(`the pipeline API closed the socket: ${code ?? ""} ${reason}`.trim());
11642
+ this.name = "PipelineSocketClosedError";
11643
+ this.code = code;
11644
+ this.reason = reason;
11083
11645
  }
11084
- async processV1(source, params) {
11085
- if (source.file !== void 0) {
11086
- return this.uploadFile(source, { componentParams: params });
11087
- } else if (source.stream !== void 0) {
11088
- return this.uploadStream(source, { componentParams: params });
11089
- } else if (source.path !== void 0) {
11090
- return this.uploadPath(source, { componentParams: params });
11091
- } else if (source.url !== void 0) {
11092
- return this.loadFrom(source, { componentParams: params });
11093
- } else if (source.assetUuid !== void 0) {
11094
- return this.loadFromAssetUuid(source, { componentParams: params });
11095
- } else if (source.mediaStream !== void 0) {
11096
- return this.loadMediaStream(source, { componentParams: params });
11097
- } else {
11098
- return Promise.reject("unknown source type");
11099
- }
11646
+ };
11647
+ var PipelineCommandError = class extends Error {
11648
+ constructor(error, detail = "") {
11649
+ super(`the pipeline API refused the command: ${error} ${detail}`.trim());
11650
+ this.name = "PipelineCommandError";
11651
+ this.error = error;
11652
+ this.detail = detail;
11100
11653
  }
11101
- /**
11102
- * Reject a source's calibration before any work is started for it.
11103
- *
11104
- * Called by every entry point that takes ProcessParams rather than at the
11105
- * one boundary they share, because they have none: the group methods build
11106
- * their jobs directly instead of going through processV2. It runs before
11107
- * the concurrency limit is acquired, so a request that can never be sent
11108
- * does not hold a slot.
11109
- */
11110
- validateProcessParams(params) {
11111
- if (params.camera !== void 0) {
11112
- validateCamera(params.camera);
11113
- }
11654
+ };
11655
+ var PipelineSourceError = class extends Error {
11656
+ constructor(sourceId, reason, detail = "") {
11657
+ super(`Error in source ${sourceId}: ${reason} ${detail}`.trim());
11658
+ this.name = "PipelineSourceError";
11659
+ this.sourceId = sourceId;
11660
+ this.reason = reason;
11661
+ this.detail = detail;
11114
11662
  }
11115
- async processV2(request) {
11116
- this.validateProcessParams(request);
11117
- if (request.source.file !== void 0) {
11118
- return this.uploadFile(request.source, request);
11119
- } else if (request.source.stream !== void 0) {
11120
- return this.uploadStream(request.source, request);
11121
- } else if (request.source.path !== void 0) {
11122
- return this.uploadPath(request.source, request);
11123
- } else if (request.source.url !== void 0) {
11124
- return this.loadFrom(request.source, request);
11125
- } else if (request.source.assetUuid !== void 0) {
11126
- return this.loadFromAssetUuid(request.source, request);
11127
- } else if (request.source.mediaStream !== void 0) {
11128
- return this.loadMediaStream(request.source, request);
11663
+ };
11664
+ var CANCELLED = /* @__PURE__ */ Symbol("cancelled");
11665
+ var Inbox = class {
11666
+ constructor() {
11667
+ this._items = [];
11668
+ this._waiter = null;
11669
+ }
11670
+ push(item) {
11671
+ const waiter = this._waiter;
11672
+ if (waiter) {
11673
+ this._waiter = null;
11674
+ waiter(item);
11129
11675
  } else {
11130
- return Promise.reject("unknown source type");
11676
+ this._items.push(item);
11131
11677
  }
11132
11678
  }
11133
- async uploadGroup(paths, params = {}) {
11134
- if (paths.length === 0) {
11135
- throw new Error("upload group requires at least one path");
11679
+ next() {
11680
+ const item = this._items.shift();
11681
+ if (item !== void 0) {
11682
+ return Promise.resolve(item);
11136
11683
  }
11137
- this.validateProcessParams(params);
11138
- if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
11139
- throw new Error("endpoint not connected, use connect()");
11684
+ return new Promise((resolve) => this._waiter = resolve);
11685
+ }
11686
+ };
11687
+ var UploadEnded = class extends Error {
11688
+ };
11689
+ var Credit = class {
11690
+ constructor() {
11691
+ this._credit = 0;
11692
+ this._ended = false;
11693
+ this._waiter = null;
11694
+ }
11695
+ grant(n) {
11696
+ this._credit += n;
11697
+ this.wake();
11698
+ }
11699
+ end() {
11700
+ this._ended = true;
11701
+ this.wake();
11702
+ }
11703
+ async take(most) {
11704
+ while (this._credit <= 0 && !this._ended) {
11705
+ await new Promise((resolve) => this._waiter = resolve);
11140
11706
  }
11141
- await this._limit.acquire();
11142
- try {
11143
- this.updateState();
11144
- const sources = await Promise.all(
11145
- paths.map(async (path2) => {
11146
- let streamSource;
11147
- if (this._options.platformSupport?.resolvePath) {
11148
- streamSource = await this._options.platformSupport.resolvePath({ path: path2 }, this._logger);
11149
- } else {
11150
- streamSource = await resolvePath({ path: path2 }, this._logger);
11151
- }
11152
- return { stream: streamSource.stream, mimeType: streamSource.mimeType };
11153
- })
11154
- );
11155
- const job = new UploadGroupJob(sources, params, async () => this.session(), this._client, this._requestLogger, this.predictionVersion());
11156
- return job.start(
11157
- () => {
11158
- this.jobDone(job);
11159
- },
11160
- (statusCode) => {
11161
- this.jobStatus(job, statusCode);
11162
- }
11163
- );
11164
- } catch (e) {
11165
- this._limit.release();
11166
- throw e;
11707
+ if (this._ended) {
11708
+ throw new UploadEnded();
11167
11709
  }
11710
+ const n = Math.min(most, this._credit);
11711
+ this._credit -= n;
11712
+ return n;
11168
11713
  }
11169
- async uploadStreamGroup(streams, mimeTypes, params = {}) {
11170
- if (streams.length === 0) {
11171
- throw new Error("upload stream group requires at least one stream");
11714
+ wake() {
11715
+ const waiter = this._waiter;
11716
+ this._waiter = null;
11717
+ waiter?.();
11718
+ }
11719
+ };
11720
+ var OpenSource = class {
11721
+ constructor(id) {
11722
+ this.id = id;
11723
+ this.inbox = new Inbox();
11724
+ this.uploads = /* @__PURE__ */ new Map();
11725
+ }
11726
+ close(error) {
11727
+ this.uploads.forEach((upload) => upload.end());
11728
+ this.inbox.push(error);
11729
+ }
11730
+ };
11731
+ var PipelineSocket = class {
11732
+ constructor(ws) {
11733
+ this.ws = ws;
11734
+ this.sources = /* @__PURE__ */ new Map();
11735
+ this.commands = /* @__PURE__ */ new Map();
11736
+ this._closed = false;
11737
+ this.closed = new Promise((resolve) => this._markClosed = resolve);
11738
+ }
11739
+ get open() {
11740
+ return !this._closed && this.ws.readyState === 1;
11741
+ }
11742
+ markClosed() {
11743
+ this._closed = true;
11744
+ this._markClosed();
11745
+ }
11746
+ sendJson(message) {
11747
+ this.ws.send(JSON.stringify(message));
11748
+ }
11749
+ sendBytes(data) {
11750
+ this.ws.send(data);
11751
+ }
11752
+ };
11753
+ var PipelineJob = class {
11754
+ constructor(source, media, live, params, mediaStream = void 0) {
11755
+ this.source = source;
11756
+ this.media = media;
11757
+ this.live = live;
11758
+ this.params = params;
11759
+ this.mediaStream = mediaStream;
11760
+ this._results = new Inbox();
11761
+ this._consumed = false;
11762
+ this._cancelled = false;
11763
+ this.openSource = null;
11764
+ // Stops the job's source on the pipeline, once it was sent.
11765
+ this.stopOnPipeline = null;
11766
+ }
11767
+ get cancelled() {
11768
+ return this._cancelled;
11769
+ }
11770
+ command(id) {
11771
+ const command = { type: "setSource", id, ...this.source };
11772
+ if (this.params.componentParams !== void 0) {
11773
+ command["params"] = this.params.componentParams;
11172
11774
  }
11173
- if (mimeTypes !== void 0 && mimeTypes.length !== streams.length) {
11174
- throw new Error("mimeTypes must have the same length as streams");
11775
+ if (this.params.roi !== void 0) {
11776
+ command["roi"] = this.params.roi;
11175
11777
  }
11176
- this.validateProcessParams(params);
11177
- if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
11178
- throw new Error("endpoint not connected, use connect()");
11778
+ if (this.params.camera !== void 0) {
11779
+ command["camera"] = this.params.camera;
11179
11780
  }
11180
- await this._limit.acquire();
11181
- try {
11182
- this.updateState();
11183
- const sources = streams.map((stream, i) => ({
11184
- stream,
11185
- mimeType: mimeTypes?.[i] ?? null
11186
- }));
11187
- const job = new UploadGroupJob(sources, params, async () => this.session(), this._client, this._requestLogger, this.predictionVersion());
11188
- return job.start(
11189
- () => {
11190
- this.jobDone(job);
11191
- },
11192
- (statusCode) => {
11193
- this.jobStatus(job, statusCode);
11194
- }
11195
- );
11196
- } catch (e) {
11197
- this._limit.release();
11198
- throw e;
11781
+ if (this.params.fps !== void 0) {
11782
+ command["fps"] = this.params.fps;
11199
11783
  }
11784
+ return command;
11200
11785
  }
11201
- async loadFromGroup(urls, params = {}) {
11202
- if (urls.length === 0) {
11203
- throw new Error("load from group requires at least one url");
11204
- }
11205
- this.validateProcessParams(params);
11206
- if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
11207
- throw new Error("endpoint not connected, use connect()");
11208
- }
11209
- await this._limit.acquire();
11210
- try {
11211
- this.updateState();
11212
- const job = new LoadFromGroupJob(urls, params, async () => this.session(), this._client, this._requestLogger, this.predictionVersion());
11213
- return job.start(
11214
- () => {
11215
- this.jobDone(job);
11216
- },
11217
- (statusCode) => {
11218
- this.jobStatus(job, statusCode);
11219
- }
11220
- );
11221
- } catch (e) {
11222
- this._limit.release();
11223
- throw e;
11786
+ predicted(prediction) {
11787
+ if (!this._cancelled) {
11788
+ this._results.push(prediction);
11224
11789
  }
11225
11790
  }
11226
- async uploadFile(source, params) {
11227
- await this.ensureTransientPipelineStarted();
11228
- if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
11229
- return Promise.reject("endpoint not connected, use connect()");
11230
- }
11231
- await this._limit.acquire();
11232
- try {
11233
- this.updateState();
11234
- const job = new UploadJob(
11235
- source.file,
11236
- source.file.type,
11237
- source.file.size,
11238
- source.videoMode,
11239
- params,
11240
- async () => {
11241
- return this.session();
11242
- },
11243
- this._client,
11244
- this._requestLogger,
11245
- void 0,
11246
- this.predictionVersion()
11247
- );
11248
- return job.start(
11249
- () => {
11250
- this.jobDone(job);
11251
- },
11252
- (statusCode) => {
11253
- this.jobStatus(job, statusCode);
11254
- }
11255
- );
11256
- } catch (e) {
11257
- this._limit.release();
11258
- throw e;
11791
+ finish(error = null) {
11792
+ if (error !== null) {
11793
+ this._results.push(error);
11259
11794
  }
11795
+ this._results.push(null);
11260
11796
  }
11261
- async uploadStream(source, params) {
11262
- await this.ensureTransientPipelineStarted();
11263
- if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
11264
- return Promise.reject("endpoint not connected, use connect()");
11265
- }
11266
- await this._limit.acquire();
11267
- try {
11268
- this.updateState();
11269
- const job = new UploadJob(
11270
- source.stream,
11271
- source.mimeType,
11272
- source.size,
11273
- source.videoMode,
11274
- params,
11275
- async () => {
11276
- return this.session();
11277
- },
11278
- this._client,
11279
- this._requestLogger,
11280
- void 0,
11281
- this.predictionVersion()
11282
- );
11283
- return job.start(
11284
- () => {
11285
- this.jobDone(job);
11286
- },
11287
- (statusCode) => {
11288
- this.jobStatus(job, statusCode);
11289
- }
11290
- );
11291
- } catch (e) {
11292
- this._limit.release();
11293
- throw e;
11797
+ cancel() {
11798
+ if (this._cancelled) {
11799
+ return;
11294
11800
  }
11801
+ this._cancelled = true;
11802
+ this._results.push(null);
11803
+ this.openSource?.inbox.push(CANCELLED);
11804
+ this.stopOnPipeline?.();
11295
11805
  }
11296
- async uploadPath(source, params) {
11297
- await this.ensureTransientPipelineStarted();
11298
- if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
11299
- throw new Error("endpoint not connected, use connect()");
11806
+ async *[Symbol.asyncIterator]() {
11807
+ if (this._consumed) {
11808
+ throw new Error("Cannot iterate over a consumed stream");
11300
11809
  }
11301
- await this._limit.acquire();
11302
- try {
11303
- this.updateState();
11304
- const resolveStreamSource = async () => {
11305
- if (this._options.platformSupport?.resolvePath) {
11306
- return await this._options.platformSupport.resolvePath(source, this._logger);
11307
- }
11308
- return await resolvePath(source, this._logger);
11309
- };
11310
- const streamSource = await resolveStreamSource();
11311
- let firstStreamPending = true;
11312
- const uploadBodyFactory = async () => {
11313
- if (firstStreamPending) {
11314
- firstStreamPending = false;
11315
- return streamSource.stream;
11316
- }
11317
- return (await resolveStreamSource()).stream;
11318
- };
11319
- const job = new UploadJob(
11320
- streamSource.stream,
11321
- streamSource.mimeType,
11322
- streamSource.size,
11323
- source.videoMode,
11324
- params,
11325
- async () => {
11326
- return this.session();
11327
- },
11328
- this._client,
11329
- this._requestLogger,
11330
- uploadBodyFactory,
11331
- this.predictionVersion()
11332
- );
11333
- return job.start(
11334
- () => {
11335
- this.jobDone(job);
11336
- },
11337
- (statusCode) => {
11338
- this.jobStatus(job, statusCode);
11339
- }
11340
- );
11341
- } catch (e) {
11342
- this._limit.release();
11343
- throw e;
11810
+ this._consumed = true;
11811
+ while (true) {
11812
+ const item = await this._results.next();
11813
+ if (item === null) {
11814
+ return;
11815
+ }
11816
+ if (item instanceof Error) {
11817
+ throw item;
11818
+ }
11819
+ yield item;
11344
11820
  }
11345
11821
  }
11346
- async loadFrom(source, params) {
11347
- if (source.url.startsWith("file://")) {
11348
- return await this.uploadPath(
11349
- {
11350
- path: source.url.substring("file://".length)
11351
- },
11352
- params
11353
- );
11822
+ };
11823
+ var PipelineBackend = class {
11824
+ constructor(ctx) {
11825
+ this.ctx = ctx;
11826
+ this._pipelineApiUrl = null;
11827
+ this._socket = null;
11828
+ this._connecting = null;
11829
+ // Every job's setSource and every changePop goes out through this chain, in the order submitted.
11830
+ this._sendChain = Promise.resolve();
11831
+ this._disconnecting = false;
11832
+ this._nextId = 0;
11833
+ this._warned = /* @__PURE__ */ new Set();
11834
+ this._heartbeat = null;
11835
+ // Reconnects at once since a source last played.
11836
+ this._reconnects = 0;
11837
+ this._currentPop = ctx.options.pop ?? null;
11838
+ }
11839
+ // Connection
11840
+ async reconnect() {
11841
+ this._disconnecting = false;
11842
+ await this.inOrder(() => this.ensureSocket());
11843
+ this.ctx.updateState();
11844
+ }
11845
+ async disconnect(closeEndpoint) {
11846
+ await closeEndpoint();
11847
+ this._disconnecting = true;
11848
+ if (this._heartbeat !== null) {
11849
+ clearInterval(this._heartbeat);
11850
+ this._heartbeat = null;
11354
11851
  }
11355
- await this.ensureTransientPipelineStarted();
11356
- if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
11357
- throw new Error("endpoint not connected, use connect()");
11852
+ const socket = this._socket;
11853
+ this._socket = null;
11854
+ if (socket !== null) {
11855
+ socket.ws.close(1e3);
11856
+ await socket.closed;
11358
11857
  }
11359
- await this._limit.acquire();
11360
- try {
11361
- this.updateState();
11362
- const job = new LoadFromJob(
11363
- source.url,
11364
- params,
11365
- async () => {
11366
- return this.session();
11367
- },
11368
- this._client,
11369
- this._requestLogger,
11370
- this.predictionVersion()
11371
- );
11372
- return job.start(
11373
- () => {
11374
- this.jobDone(job);
11375
- },
11376
- (statusCode) => {
11377
- this.jobStatus(job, statusCode);
11378
- }
11379
- );
11380
- } catch (e) {
11381
- this._limit.release();
11382
- throw e;
11858
+ }
11859
+ onStatus(_statusCode) {
11860
+ }
11861
+ popName() {
11862
+ return null;
11863
+ }
11864
+ async authorizationHeader() {
11865
+ return this.ctx.credentialHeader();
11866
+ }
11867
+ inOrder(work) {
11868
+ const result = this._sendChain.then(work);
11869
+ this._sendChain = result.catch(() => void 0);
11870
+ return result;
11871
+ }
11872
+ /** The open socket, or a new one with the last pop set on it. */
11873
+ async ensureSocket() {
11874
+ if (this._socket !== null && this._socket.open) {
11875
+ return this._socket;
11383
11876
  }
11877
+ if (this._connecting === null) {
11878
+ this._connecting = this.openSocket().finally(() => this._connecting = null);
11879
+ }
11880
+ return await this._connecting;
11384
11881
  }
11385
- async loadFromAssetUuid(source, params) {
11386
- await this.ensureTransientPipelineStarted();
11387
- if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
11388
- throw new Error("endpoint not connected, use connect()");
11882
+ async openSocket() {
11883
+ if (this.ctx.client() === null) {
11884
+ throw new PipelineSocketClosedError(void 0, "the endpoint is not connected");
11389
11885
  }
11390
- await this._limit.acquire();
11391
- try {
11392
- this.updateState();
11393
- const job = new LoadFromAssetUuidJob(
11394
- source.assetUuid,
11395
- params,
11396
- async () => {
11397
- return this.session();
11398
- },
11399
- this._client,
11400
- this._requestLogger,
11401
- this.predictionVersion()
11402
- );
11403
- return job.start(
11404
- () => {
11405
- this.jobDone(job);
11406
- },
11407
- (statusCode) => {
11408
- this.jobStatus(job, statusCode);
11886
+ const ws = await this.upgrade(await this.socketUrl());
11887
+ const socket = new PipelineSocket(ws);
11888
+ ws.onmessage = (event) => {
11889
+ if (typeof event.data === "string") {
11890
+ try {
11891
+ this.dispatch(socket, JSON.parse(event.data));
11892
+ } catch (error) {
11893
+ this.ctx.logger.warn("unreadable message from the pipeline API: %s", error);
11409
11894
  }
11410
- );
11411
- } catch (e) {
11412
- this._limit.release();
11413
- throw e;
11895
+ }
11896
+ };
11897
+ ws.onerror = () => {
11898
+ };
11899
+ ws.onclose = (event) => this.closed(socket, event.code, event.reason);
11900
+ this._socket = socket;
11901
+ this.startHeartbeat(ws);
11902
+ if (this._currentPop !== null) {
11903
+ try {
11904
+ await this.setPopOn(socket, this._currentPop);
11905
+ } catch (error) {
11906
+ this._socket = null;
11907
+ ws.close(1e3);
11908
+ throw error;
11909
+ }
11414
11910
  }
11911
+ return socket;
11415
11912
  }
11416
- async loadMediaStream(source, params) {
11417
- await this.ensureTransientPipelineStarted();
11418
- if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
11419
- throw new Error("endpoint not connected, use connect()");
11913
+ startHeartbeat(ws) {
11914
+ if (this._heartbeat !== null) {
11915
+ clearInterval(this._heartbeat);
11420
11916
  }
11421
- await this._limit.acquire();
11422
- try {
11423
- this.updateState();
11424
- const job = new LoadMediaStreamJob(
11425
- source.mediaStream,
11426
- params,
11427
- async () => {
11428
- return this.session();
11429
- },
11430
- this._client,
11431
- this._requestLogger,
11432
- this.predictionVersion()
11433
- );
11434
- return job.start(
11435
- () => {
11436
- this.jobDone(job);
11437
- },
11438
- (statusCode) => {
11439
- this.jobStatus(job, statusCode);
11917
+ const ping = ws.ping;
11918
+ if (typeof ping === "function") {
11919
+ this._heartbeat = setInterval(() => {
11920
+ if (ws.readyState === 1) {
11921
+ ping.call(ws);
11440
11922
  }
11441
- );
11442
- } catch (e) {
11443
- this._limit.release();
11444
- throw e;
11923
+ }, HEARTBEAT_MS);
11924
+ this._heartbeat.unref?.();
11445
11925
  }
11446
11926
  }
11447
- jobDone(_) {
11448
- this._limit?.release();
11449
- this.updateState();
11927
+ async socketUrl() {
11928
+ if (this._pipelineApiUrl === null) {
11929
+ const base = this.ctx.options.isLocalMode ? this.ctx.eyepopUrl() : new URL(await this.configuredPipelineApiUrl(), this.ctx.eyepopUrl() + "/").toString();
11930
+ this._pipelineApiUrl = checkedUrl(base);
11931
+ }
11932
+ return "ws" + this._pipelineApiUrl.slice("http".length) + PIPELINE_PATH;
11450
11933
  }
11451
- jobStatus(_, statusCode) {
11452
- if (statusCode == 404) {
11453
- this.statusHandler404(statusCode);
11454
- } else if (statusCode == 401) {
11455
- this.statusHandler401(statusCode);
11934
+ async configuredPipelineApiUrl() {
11935
+ const client = this.ctx.client();
11936
+ if (client === null) {
11937
+ throw new Error("endpoint not connected");
11938
+ }
11939
+ let configUrl = `${this.ctx.eyepopUrl()}/v1/configs`;
11940
+ const accountId = this.ctx.options.accountId;
11941
+ if (accountId) {
11942
+ configUrl += `?account_uuid=${encodeURIComponent(accountId)}`;
11943
+ }
11944
+ const response = await this.ctx.fetchWithRetry(async () => client.fetch(configUrl, { headers: { Authorization: await this.ctx.credentialHeader() } }));
11945
+ if (!response.ok) {
11946
+ throw new Error(`Unexpected status ${response.status} for GET ${configUrl}: ${await response.text()}`);
11947
+ }
11948
+ const config = await response.json();
11949
+ const url = config?.["pipeline_api_url"];
11950
+ if (!url) {
11951
+ throw new PipelineConfigurationError("no_pipeline_api_url", `${configUrl} has no pipeline_api_url: this compute API serves no pipeline API`);
11952
+ }
11953
+ return url;
11954
+ }
11955
+ async token() {
11956
+ if (resolveAuth(this.ctx.options)?.isLocal !== void 0) {
11957
+ return void 0;
11456
11958
  }
11959
+ return (await this.ctx.credentialHeader()).replace(/^Bearer /, "");
11457
11960
  }
11458
- async reconnect() {
11459
- if (this.options().popId == "transient" /* Transient */) {
11460
- await this.getComputeSession();
11461
- if (this.options().pop) {
11462
- this.setTransientPipelinePop();
11961
+ /** Open the socket, waiting through 503 with Retry-After and exchanging the API key again after a 401. */
11962
+ async upgrade(url) {
11963
+ const deadline = Date.now() + MAX_RETRY_MS;
11964
+ let rejected = false;
11965
+ while (true) {
11966
+ try {
11967
+ this.ctx.requestLogger.debug("before upgrade %s", url);
11968
+ return await openAuthorizedWebSocket(url, SUBPROTOCOL, await this.token());
11969
+ } catch (error) {
11970
+ if (!(error instanceof UpgradeError)) {
11971
+ throw error;
11972
+ }
11973
+ if (error.status === 401 && !rejected) {
11974
+ this.ctx.requestLogger.debug("the pipeline API answered 401, authenticating again");
11975
+ rejected = true;
11976
+ this.ctx.forgetCredential();
11977
+ continue;
11978
+ }
11979
+ 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) {
11981
+ throw error;
11982
+ }
11983
+ this.ctx.requestLogger.info("the pipeline API answered 503, retrying in %d ms", wait);
11984
+ await new Promise((resolve) => setTimeout(resolve, wait));
11463
11985
  }
11464
- } else if (this.options().sessionUuid) {
11465
- await this.getComputeSession();
11466
- } else {
11467
- await this.startWebApiSessionWithPopId();
11468
11986
  }
11469
- this.updateState();
11470
- return Promise.resolve(this);
11471
11987
  }
11472
- async startWebApiSessionWithPopId() {
11473
- if (!this._client) {
11474
- throw new Error("endpoint not initialized");
11988
+ closed(socket, code, reason) {
11989
+ socket.markClosed();
11990
+ if (this._socket === socket) {
11991
+ this._socket = null;
11475
11992
  }
11476
- const config_url = `${this.eyepopWebApiUrl()}/pops/${this.options().popId}/config?auto_start=false`;
11477
- const headers = {
11478
- Authorization: await this.authorizationHeader()
11479
- };
11480
- this.updateState("FetchConfig" /* FetchConfig */);
11481
- let response = await this._client.fetch(config_url, { headers });
11482
- if (response.status == 401) {
11483
- this._requestLogger.debug("GET %s: 401, about to retry with fresh access token", config_url);
11484
- this.statusHandler401(response.status);
11485
- const headers2 = {
11486
- Authorization: await this.authorizationHeader()
11487
- };
11488
- this.updateState("FetchConfig" /* FetchConfig */);
11489
- response = await this._client.fetch(config_url, {
11490
- headers: headers2
11491
- });
11993
+ const error = new PipelineSocketClosedError(code, reason);
11994
+ socket.sources.forEach((source) => source.close(error));
11995
+ socket.commands.forEach((command) => command.reject(error));
11996
+ socket.commands.clear();
11997
+ if (this._disconnecting) {
11998
+ return;
11492
11999
  }
11493
- if (response.status != 200) {
11494
- const message = await response.text();
11495
- if (response.status == 429) {
11496
- this.updateState("NotAvailable" /* NotAvailable */, message);
11497
- } else {
11498
- this.updateState("Error" /* Error */, message);
12000
+ this.ctx.requestLogger.debug("the pipeline API closed the socket: %s %s", code, reason);
12001
+ if (RECONNECT_AT_ONCE.has(`${code} ${reason}`)) {
12002
+ if (reason === "credential_expired") {
12003
+ this.ctx.forgetCredential();
11499
12004
  }
11500
- throw new Error(`Unexpected status ${response.status}: ${message}`);
11501
- }
11502
- let config = await response.json();
11503
- if (!config.base_url && this.options().autoStart) {
11504
- const auto_start_config_url = `${this.eyepopWebApiUrl()}/pops/${this.options().popId}/config?auto_start=true`;
11505
- this._requestLogger.debug("pop was not running, trying to autostart with: %s", auto_start_config_url);
11506
- const headers2 = {
11507
- Authorization: await this.authorizationHeader()
11508
- };
11509
- response = await this._client.fetch(auto_start_config_url, {
11510
- headers: headers2
12005
+ const delay = this._reconnects === 0 ? 0 : Math.min(1e3 * 2 ** (this._reconnects - 1), MAX_RECONNECT_DELAY_MS);
12006
+ this._reconnects += 1;
12007
+ new Promise((resolve) => setTimeout(resolve, delay)).then(() => this._disconnecting ? void 0 : this.inOrder(() => this.ensureSocket())).catch((error2) => {
12008
+ this.ctx.requestLogger.info("could not reopen the pipeline API socket: %s", error2);
11511
12009
  });
11512
- if (response.status != 200) {
11513
- this.updateState("Error" /* Error */);
11514
- const message = await response.text();
11515
- throw new Error(`Unexpected status ${response.status}: ${message}`);
12010
+ }
12011
+ }
12012
+ dispatch(socket, message) {
12013
+ const type = message["type"];
12014
+ const id = message["id"];
12015
+ const source = typeof id === "string" ? socket.sources.get(id) : void 0;
12016
+ if (type === "send" || type === "credit") {
12017
+ const upload = source?.uploads.get(message["member"] ?? null);
12018
+ upload?.grant(Number(type === "send" ? message["credit"] : message["bytes"]));
12019
+ return;
12020
+ }
12021
+ const command = typeof id === "string" ? socket.commands.get(id) : void 0;
12022
+ if (command !== void 0) {
12023
+ socket.commands.delete(id);
12024
+ if (type === "error") {
12025
+ command.reject(new PipelineCommandError(message["error"] ?? "", message["message"] ?? ""));
12026
+ } else {
12027
+ command.resolve(message);
11516
12028
  }
11517
- config = await response.json();
12029
+ return;
11518
12030
  }
11519
- const baseUrl = new URL(config.base_url, this.eyepopUrl());
11520
- this._baseUrl = baseUrl.toString();
11521
- if (this._baseUrl) {
11522
- this._baseUrl = this._baseUrl.replace(/\/+$/, "");
11523
- this._pipelineId = config.pipeline_id;
11524
- this._popName = config.name;
11525
- if (!this._pipelineId || !this._baseUrl) {
11526
- throw new Error("Pop not started");
12031
+ if (source !== void 0) {
12032
+ if (type === "start") {
12033
+ this._reconnects = 0;
11527
12034
  }
11528
- const get_url = `${this._baseUrl}/pipelines/${this._pipelineId}`;
11529
- const headers2 = {
11530
- Authorization: await this.authorizationHeader()
11531
- };
11532
- let response2 = await this._client.fetch(get_url, {
11533
- method: "GET",
11534
- headers: headers2
11535
- });
11536
- if (response2.status > 200) {
11537
- const message = await response2.text();
11538
- throw new Error(`Unexpected status ${response2.status}: ${message}`);
12035
+ if (type === "end") {
12036
+ source.uploads.forEach((upload) => upload.end());
11539
12037
  }
11540
- this._pipeline = await response2.json();
11541
- if (this.options().stopJobs) {
11542
- const body = { sourceType: "NONE" };
11543
- const headers3 = {
11544
- Authorization: await this.authorizationHeader(),
11545
- "Content-Type": "application/json"
11546
- };
11547
- const stop_url = `${this._baseUrl}/pipelines/${this._pipeline.id}/source?mode=preempt&processing=sync`;
11548
- let response3 = await this._client.fetch(stop_url, {
11549
- method: "PATCH",
11550
- headers: headers3,
11551
- body: JSON.stringify(body)
11552
- });
11553
- if (response3.status >= 300) {
11554
- const message = await response3.text();
11555
- throw new Error(`Unexpected status ${response3.status}: ${message}`);
11556
- }
12038
+ source.inbox.push(message);
12039
+ } else if (type === "error") {
12040
+ if (id === void 0) {
12041
+ this.ctx.logger.warn("the pipeline API refused a message: %s %s", message["error"], message["message"]);
12042
+ } else {
12043
+ this.ctx.requestLogger.debug("the pipeline API refused a command for %s: %s %s", id, message["error"], message["message"]);
11557
12044
  }
11558
12045
  }
11559
12046
  }
11560
- sessionReadyTimeoutMs() {
11561
- const seconds = this.options().sessionReadyTimeoutSeconds;
11562
- return typeof seconds == "number" && Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : _WorkerEndpoint.WAIT_FOR_IS_READY;
12047
+ newId(prefix) {
12048
+ this._nextId += 1;
12049
+ return `${prefix}-${this._nextId}`;
11563
12050
  }
11564
- async getComputeSession() {
11565
- if (!this._client) {
11566
- throw new Error("endpoint not initialized");
12051
+ /** Send a command that is answered by `result` or `error` under its id. */
12052
+ command(socket, message) {
12053
+ const id = this.newId(message["type"]);
12054
+ return new Promise((resolve, reject) => {
12055
+ socket.commands.set(id, { resolve, reject });
12056
+ socket.sendJson({ ...message, id });
12057
+ });
12058
+ }
12059
+ // Pop
12060
+ pop() {
12061
+ return this._currentPop;
12062
+ }
12063
+ /** Set the pop on the socket's pipeline; a pop it refuses rejects, and the last pop it took stays. */
12064
+ async changePop(pop) {
12065
+ if (this.ctx.client() === null) {
12066
+ this._currentPop = pop;
12067
+ return;
11567
12068
  }
11568
- let resolved;
11569
- try {
11570
- resolved = await new ComputeSessionClient({
11571
- computeUrl: this.eyepopUrl(),
11572
- httpClient: this._client,
11573
- authorizationHeader: () => this.computeAuthorizationHeader(),
11574
- sessionUuid: this.options().sessionUuid,
11575
- sessionName: this.options().sessionName,
11576
- accountId: this.options().accountId,
11577
- pipelineImage: this.options().pipelineImage,
11578
- pipelineVersion: this.options().pipelineVersion,
11579
- pop: this.options().pop,
11580
- readyTimeoutMs: this.sessionReadyTimeoutMs(),
11581
- logger: this._logger,
11582
- requestLogger: this._requestLogger
11583
- }).resolve();
11584
- } catch (error) {
11585
- this.updateState("Error" /* Error */);
11586
- throw error;
12069
+ await this.inOrder(async () => {
12070
+ if (this._socket !== null && this._socket.open) {
12071
+ await this.setPopOn(this._socket, pop);
12072
+ this._currentPop = pop;
12073
+ return;
12074
+ }
12075
+ const previous = this._currentPop;
12076
+ this._currentPop = pop;
12077
+ try {
12078
+ await this.ensureSocket();
12079
+ } catch (error) {
12080
+ this._currentPop = previous;
12081
+ throw error;
12082
+ }
12083
+ });
12084
+ }
12085
+ async setPopOn(socket, pop) {
12086
+ await this.command(socket, { type: "setPop", pop });
12087
+ }
12088
+ // Jobs
12089
+ async submit(work) {
12090
+ if (this.ctx.client() === null) {
12091
+ throw new Error("endpoint not connected, use connect()");
11587
12092
  }
11588
- this._sessionAccessToken = resolved.accessToken;
11589
- this._sessionAccessTokenValidUntil = resolved.accessTokenValidUntil;
11590
- this._baseUrl = resolved.session.session_endpoint;
11591
- this._accountId = resolved.session.account_uuid || void 0;
11592
- this._pipeline = null;
11593
- this._pipelineId = resolved.pipelineId;
11594
- if (this.options().pop) {
11595
- this.setTransientPipelinePop();
12093
+ const sourceOptions = work.kind === "source" ? work.source : void 0;
12094
+ this.notSent({ motionDetect: work.params.motionDetect, videoMode: sourceOptions?.videoMode, rtspForceNonCompliantUrl: work.params.rtspForceNonCompliantUrl });
12095
+ const job = await this.jobOf(work);
12096
+ const limit = this.ctx.limit();
12097
+ await limit.acquire();
12098
+ this.ctx.updateState();
12099
+ this.run(job).then(
12100
+ () => job.finish(),
12101
+ (error) => job.finish(error instanceof Error ? error : new Error(String(error)))
12102
+ ).finally(() => this.ctx.jobDone());
12103
+ return job;
12104
+ }
12105
+ notSent(options) {
12106
+ for (const [name, value] of Object.entries(options)) {
12107
+ if (value === void 0 || this._warned.has(name)) {
12108
+ continue;
12109
+ }
12110
+ this._warned.add(name);
12111
+ const ticket = name === "rtspForceNonCompliantUrl" ? "" : " (AWSU-317)";
12112
+ this.ctx.logger.warn(`${name} is not sent to the pipeline API and has no effect in pipeline mode${ticket}`);
12113
+ }
12114
+ }
12115
+ async jobOf(work) {
12116
+ switch (work.kind) {
12117
+ case "source":
12118
+ return await this.sourceJob(work.source, work.params);
12119
+ case "uploadGroup": {
12120
+ const media = await Promise.all(work.paths.map((path2) => this.pathMedia({ path: path2 })));
12121
+ return new PipelineJob(uploadGroup(media), media, false, work.params);
12122
+ }
12123
+ case "uploadStreamGroup": {
12124
+ const media = work.streams.map((stream, i) => bodyMedia(stream, work.mimeTypes?.[i] ?? "application/octet-stream", void 0));
12125
+ return new PipelineJob(uploadGroup(media), media, false, work.params);
12126
+ }
12127
+ case "loadFromGroup":
12128
+ return new PipelineJob({ group: work.urls.map((url) => ({ url })) }, [], false, work.params);
11596
12129
  }
11597
12130
  }
11598
- async startTransientPipeline() {
11599
- const client = this._client;
11600
- const baseUrl = this._baseUrl;
11601
- if (client == null || baseUrl == null) {
11602
- throw new Error("endpoint not initialized");
11603
- }
11604
- let pop;
11605
- if (this.options().pop) {
11606
- pop = this.options().pop;
11607
- } else if (this._pipeline && this._pipeline.pop) {
11608
- pop = this._pipeline.pop;
11609
- } else {
11610
- throw new Error("Cannot start a transient pipeline without a pop");
12131
+ async sourceJob(source, params) {
12132
+ if (source.file !== void 0) {
12133
+ const file = source.file;
12134
+ const media = bodyMedia(file, file.type || "application/octet-stream", file.size);
12135
+ return new PipelineJob({ upload: { contentType: media.contentType } }, [media], false, params);
12136
+ } else if (source.stream !== void 0) {
12137
+ const stream = source;
12138
+ const media = bodyMedia(stream.stream, stream.mimeType, stream.size ?? knownSize(stream.stream));
12139
+ return new PipelineJob({ upload: { contentType: media.contentType } }, [media], false, params);
12140
+ } else if (source.path !== void 0) {
12141
+ const media = await this.pathMedia(source);
12142
+ return new PipelineJob({ upload: { contentType: media.contentType } }, [media], false, params);
12143
+ } else if (source.url !== void 0) {
12144
+ const url = source.url;
12145
+ if (url.startsWith("file://")) {
12146
+ return await this.sourceJob({ path: url.substring("file://".length) }, params);
12147
+ }
12148
+ return new PipelineJob({ url }, [], /^(rtsp|rtmp):\/\//.test(url), params);
12149
+ } else if (source.assetUuid !== void 0) {
12150
+ return new PipelineJob({ assetUuid: source.assetUuid }, [], false, params);
12151
+ } else if (source.mediaStream !== void 0) {
12152
+ const ingressId = this.newId("whip");
12153
+ return new PipelineJob({ whip: { ingressId } }, [], true, params, source.mediaStream);
11611
12154
  }
11612
- const body = {
11613
- pop,
11614
- source: {
11615
- sourceType: "NONE"
11616
- },
11617
- idleTimeoutSeconds: 30,
11618
- logging: ["out_meta"],
11619
- videoOutput: "no_output"
11620
- };
11621
- const post_url = `${baseUrl.replace(/\/+$/, "")}/pipelines`;
11622
- const response = await this.fetchWithRetry(async () => {
11623
- let headers = {
11624
- Accept: "application/json",
11625
- "Content-Type": "application/json",
11626
- Authorization: await this.authorizationHeader()
11627
- };
11628
- return client.fetch(post_url, {
11629
- method: "POST",
11630
- body: JSON.stringify(body),
11631
- headers
11632
- });
11633
- });
11634
- if (response.status != 200) {
11635
- const message = await response.text();
11636
- throw new Error(`Unexpected status ${response.status} starting transient pipeline: ${message.slice(0, 300)}`);
12155
+ throw new Error("unknown source type");
12156
+ }
12157
+ async pathMedia(source) {
12158
+ const options = this.ctx.options;
12159
+ const resolve = async () => options.platformSupport?.resolvePath ? await options.platformSupport.resolvePath(source, this.ctx.logger) : await resolvePath(source, this.ctx.logger);
12160
+ const first = await resolve();
12161
+ let firstPending = true;
12162
+ return {
12163
+ contentType: first.mimeType,
12164
+ size: first.size,
12165
+ open: async () => {
12166
+ if (firstPending) {
12167
+ firstPending = false;
12168
+ return first.stream;
12169
+ }
12170
+ return (await resolve()).stream;
12171
+ }
12172
+ };
12173
+ }
12174
+ /** Run the job's source to its end; a live source refused overloaded goes once more on a new socket. */
12175
+ async run(job) {
12176
+ let retried = false;
12177
+ while (true) {
12178
+ const sent = await this.inOrder(() => this.setSource(job));
12179
+ if (sent === null) {
12180
+ return;
12181
+ }
12182
+ const { socket, source } = sent;
12183
+ const uploading = source.uploads.size > 0 ? this.upload(socket, source, job) : Promise.resolve();
12184
+ let whip = null;
12185
+ let overloaded;
12186
+ try {
12187
+ overloaded = await this.follow(job, source, () => {
12188
+ if (job.mediaStream !== void 0) {
12189
+ const fail = (error) => {
12190
+ job.stopOnPipeline?.();
12191
+ source.inbox.push(error instanceof Error ? error : new Error(String(error)));
12192
+ };
12193
+ whip = new SocketWhip(job.mediaStream, job.source["whip"]["ingressId"], (message) => this.command(socket, message), fail, this.ctx.requestLogger);
12194
+ whip.start().catch(fail);
12195
+ }
12196
+ });
12197
+ } finally {
12198
+ job.openSource = null;
12199
+ job.stopOnPipeline = null;
12200
+ socket.sources.delete(source.id);
12201
+ source.uploads.forEach((upload) => upload.end());
12202
+ await uploading;
12203
+ await whip?.close();
12204
+ }
12205
+ if (!overloaded) {
12206
+ return;
12207
+ }
12208
+ if (retried || !job.live) {
12209
+ throw new PipelineCommandError("overloaded", "the pipeline API has no capacity for another live source");
12210
+ }
12211
+ retried = true;
12212
+ await socket.closed;
11637
12213
  }
11638
- const result = await response.json();
11639
- return result["id"];
11640
12214
  }
11641
- async ensureTransientPipelineStarted() {
11642
- if (this.options().popId != "transient" /* Transient */ || this._pipelineId) {
11643
- return;
12215
+ /** Send the job's setSource; the caller runs it in submission order. */
12216
+ async setSource(job) {
12217
+ if (job.cancelled) {
12218
+ return null;
11644
12219
  }
11645
- if (!this.options().pop) {
11646
- return;
12220
+ const combined = await combinedMedia(job);
12221
+ const socket = await this.ensureSocket();
12222
+ const source = new OpenSource(this.newId("source"));
12223
+ if (combined === null) {
12224
+ uploadKeys(job).forEach((key) => source.uploads.set(key, new Credit()));
12225
+ }
12226
+ socket.sources.set(source.id, source);
12227
+ job.openSource = source;
12228
+ job.stopOnPipeline = () => {
12229
+ if (socket.open) {
12230
+ socket.sendJson({ type: "stopSource", id: source.id });
12231
+ }
12232
+ };
12233
+ const command = job.command(source.id);
12234
+ if (combined === null) {
12235
+ socket.sendJson(command);
12236
+ } else {
12237
+ socket.sendBytes(combinedMessage(command, combined));
11647
12238
  }
11648
- if (!this._baseUrl) {
11649
- await this.reconnect();
12239
+ return { socket, source };
12240
+ }
12241
+ /** Hand the source's predictions to the job until it ends; true when the pipeline API was overloaded. */
12242
+ async follow(job, source, onResult) {
12243
+ while (true) {
12244
+ if (job.cancelled) {
12245
+ return false;
12246
+ }
12247
+ const message = await source.inbox.next();
12248
+ if (message === CANCELLED) {
12249
+ return false;
12250
+ }
12251
+ if (message instanceof Error) {
12252
+ throw message;
12253
+ }
12254
+ switch (message["type"]) {
12255
+ case "result":
12256
+ onResult();
12257
+ break;
12258
+ case "prediction":
12259
+ job.predicted(message["prediction"]);
12260
+ break;
12261
+ case "end":
12262
+ if (message["reason"] === "eos") {
12263
+ return false;
12264
+ }
12265
+ throw new PipelineSourceError(source.id, message["reason"] ?? "", message["message"] ?? "");
12266
+ case "error":
12267
+ if (message["error"] === "overloaded") {
12268
+ return true;
12269
+ }
12270
+ throw new PipelineCommandError(message["error"] ?? "", message["message"] ?? "");
12271
+ case "dropped":
12272
+ this.ctx.requestLogger.debug("the pipeline API dropped %s predictions of a live source", message["count"]);
12273
+ break;
12274
+ }
11650
12275
  }
11651
- if (this._pipelineId) {
11652
- return;
12276
+ }
12277
+ /** Send each upload member's bytes in chunks within its credit, then its uploaded. */
12278
+ async upload(socket, source, job) {
12279
+ const keys = uploadKeys(job);
12280
+ try {
12281
+ for (let i = 0; i < keys.length; i++) {
12282
+ const key = keys[i];
12283
+ const credit = source.uploads.get(key);
12284
+ for await (let piece of pieces(await job.media[i].open())) {
12285
+ while (piece.length > 0) {
12286
+ const n = await credit.take(Math.min(piece.length, CHUNK_BYTES));
12287
+ const chunk = new Uint8Array(n + 1);
12288
+ chunk[0] = CHUNK;
12289
+ chunk.set(piece.subarray(0, n), 1);
12290
+ socket.sendBytes(chunk);
12291
+ piece = piece.subarray(n);
12292
+ }
12293
+ }
12294
+ socket.sendJson(key === null ? { type: "uploaded", id: source.id } : { type: "uploaded", id: source.id, member: key });
12295
+ }
12296
+ } catch (error) {
12297
+ if (!(error instanceof UploadEnded)) {
12298
+ if (socket.open) {
12299
+ source.inbox.push(error instanceof Error ? error : new Error(String(error)));
12300
+ }
12301
+ }
11653
12302
  }
11654
- if (this.options().pop) {
11655
- await this.reconnect();
11656
- if (!this._pipelineId) {
11657
- throw new Error("Compute session did not provide a pipeline for the requested pop");
12303
+ }
12304
+ async session() {
12305
+ const token = await this.token();
12306
+ const validUntil = token === void 0 ? Number.MAX_VALUE : Math.min((await this.ctx.credentialSession()).validUntil, Number.MAX_VALUE);
12307
+ return {
12308
+ eyepopUrl: this.ctx.eyepopUrl(),
12309
+ accessToken: token ?? "",
12310
+ validUntil,
12311
+ popId: "transient",
12312
+ accountId: this.ctx.options.accountId,
12313
+ baseUrl: this._pipelineApiUrl ?? void 0,
12314
+ pipelineId: void 0,
12315
+ isPipelineMode: true,
12316
+ authenticationHeaders: () => token === void 0 ? {} : { Authorization: `Bearer ${token}` }
12317
+ };
12318
+ }
12319
+ };
12320
+ var SocketWhip = class {
12321
+ constructor(_stream, _ingressId, _command, _fail, _logger) {
12322
+ this._stream = _stream;
12323
+ this._ingressId = _ingressId;
12324
+ this._command = _command;
12325
+ this._fail = _fail;
12326
+ this._logger = _logger;
12327
+ this._pc = null;
12328
+ this._answered = false;
12329
+ this._queued = [];
12330
+ this._offerData = null;
12331
+ }
12332
+ async start() {
12333
+ const servers = await this._command({ type: "whipIceServers", ingressId: this._ingressId });
12334
+ const iceServers = servers["iceServers"] ?? [];
12335
+ this._logger.debug("whip %s: ice servers %j", this._ingressId, iceServers.map((server) => server.split(";")[0]));
12336
+ const pc = new RTCPeerConnection({ iceServers: WebrtcBase.linkToIceServers(iceServers.join(", ") || null) });
12337
+ this._pc = pc;
12338
+ pc.oniceconnectionstatechange = () => {
12339
+ this._logger.debug("whip %s: ice connection %s", this._ingressId, pc.iceConnectionState);
12340
+ if (pc.iceConnectionState === "failed" && this._pc === pc) {
12341
+ this._fail(
12342
+ new Error(
12343
+ `the WebRTC connection of whip ingress ${this._ingressId} failed: no candidate pair connected, so the pipeline API is likely reachable through no STUN or TURN server (ice servers: ${iceServers.length})`
12344
+ )
12345
+ );
12346
+ }
12347
+ };
12348
+ pc.onicecandidate = (evt) => {
12349
+ if (evt.candidate !== null) {
12350
+ this.candidate(evt.candidate).catch((error) => this._logger.debug("whipIce failed: %s", error));
11658
12351
  }
12352
+ };
12353
+ this._stream.getTracks().forEach((track) => pc.addTrack(track, this._stream));
12354
+ const offer = await pc.createOffer();
12355
+ if (!offer.sdp) {
12356
+ throw new Error("the local offer has no sdp");
12357
+ }
12358
+ this._offerData = WebrtcBase.parseOffer(offer.sdp);
12359
+ await pc.setLocalDescription(offer);
12360
+ const answer = await this._command({ type: "whipOffer", ingressId: this._ingressId, sdp: offer.sdp });
12361
+ await pc.setRemoteDescription({ type: "answer", sdp: answer["sdp"] });
12362
+ this._answered = true;
12363
+ const queued = this._queued;
12364
+ this._queued = [];
12365
+ if (queued.length > 0) {
12366
+ await this.sendCandidates(queued);
12367
+ }
12368
+ }
12369
+ async candidate(candidate) {
12370
+ if (!this._answered) {
12371
+ this._queued.push(candidate);
11659
12372
  return;
11660
12373
  }
11661
- if (!this._pipelineCreatePromise) {
11662
- this._pipelineCreatePromise = this.startTransientPipeline();
12374
+ await this.sendCandidates([candidate]);
12375
+ }
12376
+ async sendCandidates(candidates) {
12377
+ if (this._offerData === null) {
12378
+ return;
11663
12379
  }
12380
+ await this._command({ type: "whipIce", ingressId: this._ingressId, fragment: WebrtcBase.generateSdpFragment(this._offerData, candidates) });
12381
+ }
12382
+ async close() {
12383
+ this._stream.getTracks().forEach((track) => track.stop());
12384
+ const pc = this._pc;
12385
+ this._pc = null;
12386
+ pc?.close();
12387
+ }
12388
+ };
12389
+ function checkedUrl(url) {
12390
+ const parsed = new URL(url);
12391
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
12392
+ throw new PipelineConfigurationError("insecure_url", `the pipeline API url ${url} must be an http or https url`);
12393
+ }
12394
+ if (parsed.protocol === "http:" && !isLoopback(parsed.hostname)) {
12395
+ throw new PipelineConfigurationError("insecure_url", `the pipeline API url ${url} must use https unless its host is a loopback address`);
12396
+ }
12397
+ return url.replace(/\/+$/, "");
12398
+ }
12399
+ function isLoopback(host) {
12400
+ return host === "localhost" || host === "[::1]" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host);
12401
+ }
12402
+ function knownSize(body) {
12403
+ if (typeof Blob !== "undefined" && body instanceof Blob) {
12404
+ return body.size;
12405
+ }
12406
+ if (ArrayBuffer.isView(body) || body instanceof ArrayBuffer) {
12407
+ return body.byteLength;
12408
+ }
12409
+ return void 0;
12410
+ }
12411
+ function bodyMedia(body, contentType, size) {
12412
+ return { open: async () => body, contentType, size: size ?? knownSize(body) };
12413
+ }
12414
+ function uploadGroup(media) {
12415
+ return { group: media.map((m) => ({ upload: { contentType: m.contentType } })) };
12416
+ }
12417
+ function uploadKeys(job) {
12418
+ if (job.source["upload"] !== void 0) {
12419
+ return [null];
12420
+ }
12421
+ const group = job.source["group"] ?? [];
12422
+ return group.flatMap((member, i) => member["upload"] !== void 0 ? [i] : []);
12423
+ }
12424
+ async function combinedMedia(job) {
12425
+ if (job.media.length === 0 || job.live || job.media.some((m) => m.size === void 0)) {
12426
+ return null;
12427
+ }
12428
+ if (job.media.reduce((total, m) => total + (m.size ?? 0), 0) > COMBINED_UPLOAD_MAX_BYTES) {
12429
+ return null;
12430
+ }
12431
+ const bytes = [];
12432
+ for (const media of job.media) {
12433
+ bytes.push(await readAll(await media.open()));
12434
+ }
12435
+ return bytes;
12436
+ }
12437
+ async function readAll(body) {
12438
+ const parts = [];
12439
+ for await (const piece of pieces(body)) {
12440
+ parts.push(piece);
12441
+ }
12442
+ return concat(parts);
12443
+ }
12444
+ function concat(parts) {
12445
+ const all = new Uint8Array(parts.reduce((total, p) => total + p.length, 0));
12446
+ let offset = 0;
12447
+ for (const part of parts) {
12448
+ all.set(part, offset);
12449
+ offset += part.length;
12450
+ }
12451
+ return all;
12452
+ }
12453
+ function combinedMessage(command, media) {
12454
+ if (command["group"] !== void 0) {
12455
+ let i = 0;
12456
+ command = {
12457
+ ...command,
12458
+ group: command["group"].map((member) => member["upload"] !== void 0 ? { upload: { ...member["upload"], bytes: media[i++].length } } : member)
12459
+ };
12460
+ }
12461
+ const header = new TextEncoder().encode(JSON.stringify(command));
12462
+ const prefix = new Uint8Array(5);
12463
+ prefix[0] = COMBINED;
12464
+ new DataView(prefix.buffer).setUint32(1, header.length, false);
12465
+ return concat([prefix, header, ...media]);
12466
+ }
12467
+ async function* pieces(body) {
12468
+ if (typeof Blob !== "undefined" && body instanceof Blob) {
12469
+ yield* pieces(body.stream());
12470
+ } else if (body instanceof ArrayBuffer) {
12471
+ yield new Uint8Array(body);
12472
+ } else if (ArrayBuffer.isView(body)) {
12473
+ yield new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
12474
+ } else {
12475
+ const reader = body.getReader();
11664
12476
  try {
11665
- this._pipelineId = await this._pipelineCreatePromise;
11666
- this.setTransientPipelinePop();
12477
+ while (true) {
12478
+ const { value, done } = await reader.read();
12479
+ if (done) {
12480
+ return;
12481
+ }
12482
+ if (value !== void 0 && value.length > 0) {
12483
+ yield value;
12484
+ }
12485
+ }
11667
12486
  } finally {
11668
- this._pipelineCreatePromise = null;
12487
+ reader.releaseLock();
11669
12488
  }
11670
12489
  }
11671
- async changeTransientPop(pop) {
11672
- if (!this._client) {
11673
- throw new Error("endpoint not initialized");
11674
- }
11675
- const previousPop = this.options().pop;
11676
- const previousBaseUrl = this._baseUrl;
11677
- const previousSessionAccessToken = this._sessionAccessToken;
11678
- const previousSessionAccessTokenValidUntil = this._sessionAccessTokenValidUntil;
11679
- if (this._pipelineId) {
11680
- await this.deleteTransientPipeline(this._pipelineId);
11681
- this._pipelineId = null;
11682
- }
11683
- this._pipeline = null;
11684
- try {
11685
- this.options().pop = pop;
11686
- await this.getComputeSession();
11687
- } catch (error) {
11688
- this.options().pop = previousPop;
11689
- this._pipeline = null;
11690
- this._pipelineId = null;
11691
- this._baseUrl = previousPop ? null : previousBaseUrl;
11692
- this._sessionAccessToken = previousSessionAccessToken;
11693
- this._sessionAccessTokenValidUntil = previousSessionAccessTokenValidUntil;
11694
- throw error;
11695
- }
11696
- if (!this._pipelineId) {
11697
- throw new Error("Compute session did not provide a pipeline for the requested pop");
12490
+ }
12491
+
12492
+ // worker/worker_endpoint.ts
12493
+ var WorkerEndpoint = class extends Endpoint {
12494
+ constructor(options) {
12495
+ if (options.isPipelineMode) {
12496
+ options = { ...options, jobQueueLength: options.jobQueueLength || MAX_OPEN_SOURCES };
12497
+ if (options.jobQueueLength > MAX_OPEN_SOURCES) {
12498
+ throw new PipelineConfigurationError("job_queue_length", `jobQueueLength ${options.jobQueueLength} exceeds ${MAX_OPEN_SOURCES}, the open sources a pipeline API socket takes`);
12499
+ }
11698
12500
  }
11699
- this.setTransientPipelinePop();
12501
+ super(options);
12502
+ const context = {
12503
+ options,
12504
+ logger: this._logger,
12505
+ requestLogger: this._requestLogger,
12506
+ client: () => this._client,
12507
+ limit: () => this._limit,
12508
+ jobDone: () => {
12509
+ this._limit.release();
12510
+ this.updateState();
12511
+ },
12512
+ updateState: (state, message) => this.updateState(state, message),
12513
+ eyepopUrl: () => this.eyepopUrl(),
12514
+ eyepopWebApiUrl: () => this.eyepopWebApiUrl(),
12515
+ credentialHeader: () => super.authorizationHeader(),
12516
+ forgetCredential: () => this.statusHandler401(401),
12517
+ fetchWithRetry: (fetcher, retries) => this.fetchWithRetry(fetcher, retries),
12518
+ credentialSession: () => super.session()
12519
+ };
12520
+ this._backend = options.isPipelineMode ? new PipelineBackend(context) : new SessionBackend(context);
12521
+ this.setStatusRetryHandlers(/* @__PURE__ */ new Map([[404, (statusCode) => this._backend.onStatus(statusCode)]]));
11700
12522
  }
11701
- async deleteTransientPipeline(pipelineId) {
11702
- if (!this._client || !this._baseUrl) {
11703
- return;
12523
+ options() {
12524
+ return this._options;
12525
+ }
12526
+ /** What runs this endpoint's jobs. */
12527
+ backend() {
12528
+ return this._backend;
12529
+ }
12530
+ async disconnect(wait = true) {
12531
+ await this._backend.disconnect(() => super.disconnect(wait));
12532
+ }
12533
+ async session() {
12534
+ return await this._backend.session();
12535
+ }
12536
+ popName() {
12537
+ return this._backend.popName();
12538
+ }
12539
+ pop() {
12540
+ return this._backend.pop();
12541
+ }
12542
+ async changePop(pop) {
12543
+ validatePop(pop);
12544
+ await this._backend.changePop(pop);
12545
+ }
12546
+ /**
12547
+ * @deprecated Pop ids are only retained for backwards compatibility.
12548
+ * Configure transient sessions with `WorkerOptions.pop`.
12549
+ */
12550
+ popId() {
12551
+ return this.options().popId || null;
12552
+ }
12553
+ async process(param1, param2) {
12554
+ if (param1.source !== void 0) {
12555
+ const request = param1;
12556
+ this.validateProcessParams(request);
12557
+ return await this.submit({ kind: "source", source: request.source, params: request });
12558
+ } else {
12559
+ return await this.submit({ kind: "source", source: param1, params: { componentParams: param2 } });
11704
12560
  }
11705
- const deleteUrl = `${this._baseUrl}/pipelines/${pipelineId}`;
11706
- const headers = {
11707
- Authorization: await this.authorizationHeader()
11708
- };
11709
- let response = await this._client.fetch(deleteUrl, {
11710
- method: "DELETE",
11711
- headers
11712
- });
11713
- if (response.status >= 300) {
11714
- const message = await response.text();
11715
- this._logger.info(`stopping pipeline failed, status ${response.status}: ${message}`);
12561
+ }
12562
+ /**
12563
+ * Reject a source's calibration before any work is started for it.
12564
+ *
12565
+ * Called by every entry point that takes ProcessParams, before the
12566
+ * concurrency limit is acquired, so a request that can never be sent
12567
+ * does not hold a slot.
12568
+ */
12569
+ validateProcessParams(params) {
12570
+ if (params.camera !== void 0) {
12571
+ validateCamera(params.camera);
11716
12572
  }
11717
12573
  }
11718
- async authorizationHeader() {
11719
- if (this._sessionAccessToken) {
11720
- return `Bearer ${this._sessionAccessToken}`;
12574
+ async uploadGroup(paths, params = {}) {
12575
+ if (paths.length === 0) {
12576
+ throw new Error("upload group requires at least one path");
11721
12577
  }
11722
- return super.authorizationHeader();
12578
+ this.validateProcessParams(params);
12579
+ return await this.submit({ kind: "uploadGroup", paths, params });
11723
12580
  }
11724
- async computeAuthorizationHeader() {
11725
- const auth = resolveAuth(this.options());
11726
- if (auth?.isLocal !== void 0) {
11727
- return "Implicit";
12581
+ async uploadStreamGroup(streams, mimeTypes, params = {}) {
12582
+ if (streams.length === 0) {
12583
+ throw new Error("upload stream group requires at least one stream");
11728
12584
  }
11729
- const credential = apiKeyCredential(auth) ?? accessTokenCredential(auth);
11730
- if (credential !== void 0) {
11731
- return `Bearer ${credential}`;
12585
+ if (mimeTypes !== void 0 && mimeTypes.length !== streams.length) {
12586
+ throw new Error("mimeTypes must have the same length as streams");
11732
12587
  }
11733
- return super.authorizationHeader();
12588
+ this.validateProcessParams(params);
12589
+ return await this.submit({ kind: "uploadStreamGroup", streams, mimeTypes, params });
11734
12590
  }
11735
- setTransientPipelinePop() {
11736
- const pop = this.options().pop;
11737
- if (pop && this._pipelineId) {
11738
- this._pipeline = {
11739
- id: this._pipelineId,
11740
- state: "idle",
11741
- startTime: (/* @__PURE__ */ new Date()).toString(),
11742
- pop
11743
- };
12591
+ async loadFromGroup(urls, params = {}) {
12592
+ if (urls.length === 0) {
12593
+ throw new Error("load from group requires at least one url");
11744
12594
  }
12595
+ this.validateProcessParams(params);
12596
+ return await this.submit({ kind: "loadFromGroup", urls, params });
12597
+ }
12598
+ async submit(work) {
12599
+ return await this._backend.submit(work);
12600
+ }
12601
+ async reconnect() {
12602
+ await this._backend.reconnect();
12603
+ return this;
12604
+ }
12605
+ async authorizationHeader() {
12606
+ return await this._backend.authorizationHeader();
11745
12607
  }
11746
12608
  };
11747
- _WorkerEndpoint.WAIT_FOR_IS_READY = 60 * 1e3;
11748
- var WorkerEndpoint = _WorkerEndpoint;
11749
-
11750
- // shims/websockets.ts
11751
- var createWebSocket;
11752
- if ("document" in globalThis && "implementation" in globalThis.document) {
11753
- createWebSocket = (url, protocols) => {
11754
- return new WebSocket(url, protocols);
11755
- };
11756
- } else {
11757
- createWebSocket = (url, protocols) => {
11758
- const ws = require("ws");
11759
- return new ws.WebSocket(url, protocols);
11760
- };
11761
- }
11762
12609
 
11763
12610
  // data/data_endpoint.ts
11764
12611
  var import_url_join = __toESM(require("url-join"));
@@ -13426,6 +14273,13 @@ var EyePop;
13426
14273
  if (opts.isLocalMode === void 0) {
13427
14274
  opts.isLocalMode = stringToBooleanSafe(readEnv("EYEPOP_LOCAL_MODE"));
13428
14275
  }
14276
+ if (opts.isPipelineMode === void 0) {
14277
+ const shared = resolveAuth(opts)?.session;
14278
+ opts.isPipelineMode = shared?.isPipelineMode ?? stringToBooleanSafe(readEnv("EYEPOP_PIPELINE_MODE"));
14279
+ }
14280
+ if (opts.isPipelineMode) {
14281
+ return pipelineEndpoint(opts);
14282
+ }
13429
14283
  if (opts.isLocalMode) {
13430
14284
  opts.eyepopUrl = "http://127.0.0.1:8080";
13431
14285
  }
@@ -13477,6 +14331,38 @@ var EyePop;
13477
14331
  return new WorkerEndpoint(opts);
13478
14332
  }
13479
14333
  EyePop2.workerEndpoint = workerEndpoint;
14334
+ function pipelineEndpoint(opts) {
14335
+ const popId = opts.popId ?? readEnv("EYEPOP_POP_ID");
14336
+ const sessionOnly = {
14337
+ popId: popId === "transient" /* Transient */ ? void 0 : popId,
14338
+ sessionUuid: opts.sessionUuid ?? readEnv("EYEPOP_SESSION_UUID"),
14339
+ sessionName: opts.sessionName,
14340
+ pipelineImage: opts.pipelineImage,
14341
+ pipelineVersion: opts.pipelineVersion
14342
+ };
14343
+ const named = Object.entries(sessionOnly).filter(([, value]) => value).map(([name]) => name);
14344
+ if (named.length > 0) {
14345
+ throw new PipelineConfigurationError("session_options", `pipeline mode runs no worker session; it takes no ${named.join(", ")}`, named);
14346
+ }
14347
+ if (opts.isLocalMode && !opts.eyepopUrl) {
14348
+ opts.eyepopUrl = readEnv("EYEPOP_URL") || "http://127.0.0.1:9002";
14349
+ }
14350
+ if (!opts.jobQueueLength) {
14351
+ opts.jobQueueLength = MAX_OPEN_SOURCES;
14352
+ }
14353
+ _fill_default_options(opts);
14354
+ if (resolveAuth(opts) === void 0) {
14355
+ if (opts.isLocalMode) {
14356
+ opts.auth = { isLocal: true };
14357
+ } else {
14358
+ throw new Error("apiKey/accessToken/session/oAuth2 option or EYEPOP_API_KEY environment variable is required");
14359
+ }
14360
+ }
14361
+ if (opts.accountId === void 0) {
14362
+ opts.accountId = resolveAuth(opts)?.session?.accountId ?? readAccountUuidEnv();
14363
+ }
14364
+ return new WorkerEndpoint(opts);
14365
+ }
13480
14366
  function dataEndpoint(opts = {}) {
13481
14367
  _fill_default_options(opts);
13482
14368
  const auth = resolveAuth(opts);
@@ -13554,6 +14440,7 @@ var index_default = EyePop;
13554
14440
  AssetUrlType,
13555
14441
  AutoAnnotateStatus,
13556
14442
  AutoAnnotateTask,
14443
+ COMBINED_UPLOAD_MAX_BYTES,
13557
14444
  ChangeType,
13558
14445
  ComputeAccountMismatchError,
13559
14446
  ComputeApiError,
@@ -13575,6 +14462,7 @@ var index_default = EyePop;
13575
14462
  LoadFromGroupJob,
13576
14463
  LoadFromJob,
13577
14464
  LoadMediaStreamJob,
14465
+ MAX_OPEN_SOURCES,
13578
14466
  ModelExportStatus,
13579
14467
  ModelFormat,
13580
14468
  ModelPrecisionType,
@@ -13583,12 +14471,21 @@ var index_default = EyePop;
13583
14471
  ModelTrainingStage,
13584
14472
  ModelType,
13585
14473
  MotionModel,
14474
+ PIPELINE_PATH,
14475
+ PipelineBackend,
14476
+ PipelineCommandError,
14477
+ PipelineConfigurationError,
14478
+ PipelineJob,
14479
+ PipelineSocketClosedError,
14480
+ PipelineSourceError,
13586
14481
  PointCloud,
13587
14482
  PopComponentType,
13588
14483
  PredictionVersion,
13589
14484
  QUATERNION_TOLERANCE,
14485
+ SUBPROTOCOL,
13590
14486
  SelectMode,
13591
14487
  Semaphore,
14488
+ SessionBackend,
13592
14489
  Stream,
13593
14490
  TaskMode,
13594
14491
  TaskType,