moqtail 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.cjs CHANGED
@@ -5123,6 +5123,7 @@ var SubscribeRequest = class {
5123
5123
  priority;
5124
5124
  forward;
5125
5125
  subscribeParameters;
5126
+ earlyDiscardPolicy;
5126
5127
  largestLocation;
5127
5128
  // Updated on each received object
5128
5129
  streamsAccepted = 0n;
@@ -5958,6 +5959,11 @@ var handlerRequestsBlocked = async (_client, msg) => {
5958
5959
  logger.debug("handler/requests_blocked", "not implemented", msg);
5959
5960
  };
5960
5961
 
5962
+ // src/client/util/validators.ts
5963
+ function isValidTrackAlias(trackAlias) {
5964
+ return trackAlias !== void 0 && trackAlias >= 0n;
5965
+ }
5966
+
5961
5967
  // src/client/handler/subscribe.ts
5962
5968
  var handlerSubscribe = async (client, msg) => {
5963
5969
  logger.debug("handler/subscribe", `received requestId=${msg.requestId} ftn="${msg.fullTrackName}"`);
@@ -5990,7 +5996,7 @@ var handlerSubscribe = async (client, msg) => {
5990
5996
  await client.controlStream.send(response);
5991
5997
  return;
5992
5998
  }
5993
- if (!track.trackAlias) throw new Error("Expected track alias to be set");
5999
+ if (!isValidTrackAlias(track.trackAlias)) throw new Error("Expected track alias to be set");
5994
6000
  const largestLocation = track.trackSource.live.largestLocation;
5995
6001
  const parameters = [...msg.parameters];
5996
6002
  if (largestLocation) {
@@ -6456,6 +6462,8 @@ var MOQtailClient = class _MOQtailClient {
6456
6462
  #isDestroyed = false;
6457
6463
  /** Internal monotonically increasing client-assigned request id counter (even/odd parity scheme advances by 2). */
6458
6464
  #dontUseRequestId = 0n;
6465
+ /** Active early discard policy; undefined means no per-stream deadline is applied. */
6466
+ #earlyDiscardPolicy;
6459
6467
  /**
6460
6468
  * TODO: onNamespaceAnnounced may be a better name
6461
6469
  * Fired when an PUBLISH_NAMESPACE control message is processed for a track namespace.
@@ -6907,6 +6915,28 @@ var MOQtailClient = class _MOQtailClient {
6907
6915
  return FullTrackName.tryNew("unknown", `track-${trackAlias}`);
6908
6916
  }
6909
6917
  }
6918
+ /**
6919
+ * Sets (or replaces) the client-level default early discard policy for incoming subgroup streams.
6920
+ *
6921
+ * When set, each incoming subgroup QUIC stream is given a deadline of `subgroupReceiveTimeout` ms to
6922
+ * complete. If the stream has not finished within that window it is cancelled — objects already
6923
+ * delivered to the subscription are kept, but no further objects arrive from that stream.
6924
+ *
6925
+ * This is a client-wide default. Individual subscriptions can override it via the `earlyDiscardPolicy`
6926
+ * field in {@link SubscribeOptions}, which takes precedence over this setting.
6927
+ *
6928
+ * The policy takes effect on the next stream accepted after this call. Passing a new config
6929
+ * replaces the previous one. Pass `undefined` to remove the default.
6930
+ *
6931
+ * @example
6932
+ * ```ts
6933
+ * client.setEarlyDiscardPolicy({ subgroupReceiveTimeout: 2000 })
6934
+ * ```
6935
+ */
6936
+ setEarlyDiscardPolicy(config) {
6937
+ this.#ensureActive();
6938
+ this.#earlyDiscardPolicy = config;
6939
+ }
6910
6940
  /**
6911
6941
  * Gracefully terminates this {@link MOQtailClient} session and releases underlying {@link https://developer.mozilla.org/docs/Web/API/WebTransport | WebTransport} resources.
6912
6942
  *
@@ -6991,7 +7021,7 @@ var MOQtailClient = class _MOQtailClient {
6991
7021
  */
6992
7022
  addOrUpdateTrack(track) {
6993
7023
  this.#ensureActive();
6994
- if (!track.trackAlias) {
7024
+ if (!isValidTrackAlias(track.trackAlias)) {
6995
7025
  track.trackAlias = random60bitId();
6996
7026
  }
6997
7027
  this.trackSources.set(track.fullTrackName.toString(), track);
@@ -7123,6 +7153,7 @@ var MOQtailClient = class _MOQtailClient {
7123
7153
  break;
7124
7154
  }
7125
7155
  const request = new SubscribeRequest(msg);
7156
+ request.earlyDiscardPolicy = args.earlyDiscardPolicy;
7126
7157
  this.requests.set(request.requestId, request);
7127
7158
  this.requestIdMap.addMapping(request.requestId, request.fullTrackName);
7128
7159
  logger.debug("MOQtailClient", `subscribe: sending SUBSCRIBE requestId=${msg.requestId} ftn="${fullTrackName}"`);
@@ -7271,7 +7302,7 @@ var MOQtailClient = class _MOQtailClient {
7271
7302
  const request = this.requests.get(subscriptionRequestId);
7272
7303
  if (request instanceof SubscribeRequest) {
7273
7304
  const trackAlias = this.subscriptionAliasMap.get(subscriptionRequestId);
7274
- if (!trackAlias)
7305
+ if (!isValidTrackAlias(trackAlias))
7275
7306
  throw new InternalError("MOQtailClient.subscribeUpdate", "Request exists but track alias mapping does not");
7276
7307
  const subscription = this.subscriptions.get(trackAlias);
7277
7308
  if (!subscription)
@@ -7324,7 +7355,7 @@ var MOQtailClient = class _MOQtailClient {
7324
7355
  if (!(request instanceof SubscribeRequest))
7325
7356
  throw new ProtocolViolationError("MOQtailClient.switch", "Request id is not a subscription");
7326
7357
  const trackAlias = this.subscriptionAliasMap.get(subscriptionRequestId);
7327
- if (!trackAlias)
7358
+ if (!isValidTrackAlias(trackAlias))
7328
7359
  throw new InternalError("MOQtailClient.switch", "Request exists but track alias mapping does not");
7329
7360
  const subscription = this.subscriptions.get(trackAlias);
7330
7361
  if (!subscription) throw new InternalError("MOQtailClient.switch", "Request exists but subscription does not");
@@ -7988,43 +8019,55 @@ var MOQtailClient = class _MOQtailClient {
7988
8019
  if (subscription) {
7989
8020
  subscription.streamsAccepted++;
7990
8021
  let firstObjectId = null;
7991
- while (true) {
7992
- const { done, value: nextObject } = await reader.read();
7993
- if (done) {
7994
- break;
7995
- }
7996
- if (nextObject) {
7997
- if (nextObject instanceof SubgroupObject) {
7998
- if (!firstObjectId) {
7999
- firstObjectId = nextObject.objectId;
8000
- }
8001
- let subgroupId = null;
8002
- if (SubgroupHeaderType.isSubgroupIdZero(header.type)) {
8003
- subgroupId = 0n;
8004
- } else if (SubgroupHeaderType.isSubgroupIdFirstObjectId(header.type)) {
8005
- subgroupId = firstObjectId ?? null;
8006
- } else if (SubgroupHeaderType.hasExplicitSubgroupId(header.type)) {
8007
- subgroupId = header.subgroupId ?? null;
8008
- }
8009
- const fullTrackName = this.aliasFullTrackNameMap.get(header.trackAlias);
8010
- if (!fullTrackName) {
8011
- throw new ProtocolViolationError("MOQtailClient", "No full track name for received track alias");
8022
+ let subgroupTimeoutId;
8023
+ const effectiveDiscardPolicy = subscription.earlyDiscardPolicy ?? this.#earlyDiscardPolicy;
8024
+ if (effectiveDiscardPolicy?.subgroupReceiveTimeout !== void 0) {
8025
+ subgroupTimeoutId = setTimeout(() => {
8026
+ reader.cancel("early discard: subgroupReceiveTimeout exceeded").catch(() => {
8027
+ });
8028
+ }, effectiveDiscardPolicy.subgroupReceiveTimeout);
8029
+ }
8030
+ try {
8031
+ while (true) {
8032
+ const { done, value: nextObject } = await reader.read();
8033
+ if (done) {
8034
+ break;
8035
+ }
8036
+ if (nextObject) {
8037
+ if (nextObject instanceof SubgroupObject) {
8038
+ if (!firstObjectId) {
8039
+ firstObjectId = nextObject.objectId;
8040
+ }
8041
+ let subgroupId = null;
8042
+ if (SubgroupHeaderType.isSubgroupIdZero(header.type)) {
8043
+ subgroupId = 0n;
8044
+ } else if (SubgroupHeaderType.isSubgroupIdFirstObjectId(header.type)) {
8045
+ subgroupId = firstObjectId ?? null;
8046
+ } else if (SubgroupHeaderType.hasExplicitSubgroupId(header.type)) {
8047
+ subgroupId = header.subgroupId ?? null;
8048
+ }
8049
+ const fullTrackName = this.aliasFullTrackNameMap.get(header.trackAlias);
8050
+ if (!fullTrackName) {
8051
+ throw new ProtocolViolationError("MOQtailClient", "No full track name for received track alias");
8052
+ }
8053
+ const moqtObject = MoqtObject.fromSubgroupObject(
8054
+ nextObject,
8055
+ header.groupId,
8056
+ header.publisherPriority,
8057
+ subgroupId,
8058
+ fullTrackName
8059
+ );
8060
+ if (!subscription.largestLocation) subscription.largestLocation = moqtObject.location;
8061
+ if (subscription.largestLocation.compare(moqtObject.location) == -1)
8062
+ subscription.largestLocation = moqtObject.location;
8063
+ subscription.controller?.enqueue(moqtObject);
8064
+ continue;
8012
8065
  }
8013
- const moqtObject = MoqtObject.fromSubgroupObject(
8014
- nextObject,
8015
- header.groupId,
8016
- header.publisherPriority,
8017
- subgroupId,
8018
- fullTrackName
8019
- );
8020
- if (!subscription.largestLocation) subscription.largestLocation = moqtObject.location;
8021
- if (subscription.largestLocation.compare(moqtObject.location) == -1)
8022
- subscription.largestLocation = moqtObject.location;
8023
- subscription.controller?.enqueue(moqtObject);
8024
- continue;
8066
+ throw new ProtocolViolationError("MOQtailClient", "Received fetch object after subgroup header");
8025
8067
  }
8026
- throw new ProtocolViolationError("MOQtailClient", "Received fetch object after subgroup header");
8027
8068
  }
8069
+ } finally {
8070
+ if (subgroupTimeoutId !== void 0) clearTimeout(subgroupTimeoutId);
8028
8071
  }
8029
8072
  if (subscription.expectedStreams && subscription.expectedStreams === subscription.streamsAccepted) {
8030
8073
  subscription.controller?.close();
package/dist/client.d.cts CHANGED
@@ -494,6 +494,7 @@ declare class SubscribeRequest implements PromiseLike<SubscribeOk | RequestError
494
494
  priority: number;
495
495
  forward: boolean;
496
496
  subscribeParameters: MessageParameter[];
497
+ earlyDiscardPolicy: EarlyDiscardPolicyConfig | undefined;
497
498
  largestLocation: Location | undefined;
498
499
  streamsAccepted: bigint;
499
500
  expectedStreams: bigint | undefined;
@@ -716,6 +717,8 @@ type SubscribeOptions = {
716
717
  startLocation?: Location;
717
718
  /** Required for {@link FilterType.AbsoluteRange}; exclusive upper group boundary (coerced to bigint if number provided). */
718
719
  endGroup?: bigint | number;
720
+ /** Per-subscription early discard policy. Overrides the client-level default set via {@link MOQtailClient.setEarlyDiscardPolicy}. */
721
+ earlyDiscardPolicy?: EarlyDiscardPolicyConfig;
719
722
  };
720
723
  /**
721
724
  * Narrowing update constraints applied to an existing SUBSCRIBE via {@link MOQtailClient.subscribeUpdate}.
@@ -799,6 +802,13 @@ type SwitchOptions = {
799
802
  * })
800
803
  * ```
801
804
  */
805
+ /**
806
+ * Configuration for the early discard policy applied to incoming subgroup streams.
807
+ */
808
+ type EarlyDiscardPolicyConfig = {
809
+ /** Cancel a subgroup QUIC stream if it has not fully completed within this many milliseconds. */
810
+ subgroupReceiveTimeout: number;
811
+ };
802
812
  type FetchOptions = {
803
813
  /** Request priority (0 = highest, 255 = lowest). Rounded & clamped. */
804
814
  priority: number;
@@ -1277,6 +1287,25 @@ declare class MOQtailClient {
1277
1287
  * ```
1278
1288
  */
1279
1289
  sendDatagram(trackAlias: bigint, object: MoqtObject): Promise<void>;
1290
+ /**
1291
+ * Sets (or replaces) the client-level default early discard policy for incoming subgroup streams.
1292
+ *
1293
+ * When set, each incoming subgroup QUIC stream is given a deadline of `subgroupReceiveTimeout` ms to
1294
+ * complete. If the stream has not finished within that window it is cancelled — objects already
1295
+ * delivered to the subscription are kept, but no further objects arrive from that stream.
1296
+ *
1297
+ * This is a client-wide default. Individual subscriptions can override it via the `earlyDiscardPolicy`
1298
+ * field in {@link SubscribeOptions}, which takes precedence over this setting.
1299
+ *
1300
+ * The policy takes effect on the next stream accepted after this call. Passing a new config
1301
+ * replaces the previous one. Pass `undefined` to remove the default.
1302
+ *
1303
+ * @example
1304
+ * ```ts
1305
+ * client.setEarlyDiscardPolicy({ subgroupReceiveTimeout: 2000 })
1306
+ * ```
1307
+ */
1308
+ setEarlyDiscardPolicy(config: EarlyDiscardPolicyConfig | undefined): void;
1280
1309
  /**
1281
1310
  * Gracefully terminates this {@link MOQtailClient} session and releases underlying {@link https://developer.mozilla.org/docs/Web/API/WebTransport | WebTransport} resources.
1282
1311
  *
@@ -1820,4 +1849,4 @@ declare class RecvStream {
1820
1849
  static new(readStream: ReadableStream<Uint8Array>, partialDataTimeout?: number, onDataReceived?: (data: SubgroupObject | SubgroupHeader | FetchObject | FetchHeader) => void): Promise<RecvStream>;
1821
1850
  }
1822
1851
 
1823
- export { ControlStream, type FetchOptions, FetchPublication, FetchRequest, HybridTrackSource, type LiveObjectSource, LiveTrackSource, MOQtailClient, type MOQtailClientOptions, type MOQtailRequest, MemoryObjectCache, type ObjectCache, type PastObjectSource, PublishNamespaceRequest, PublishPublication, PublishRequest, RecvDatagramStream, RecvStream, RingBufferObjectCache, SendDatagramStream, SendStream, StaticTrackSource, SubscribeNamespaceRequest, type SubscribeOptions, SubscribePublication, SubscribeRequest, type SubscribeUpdateOptions, type SwitchOptions, type Track, type TrackSource, TrackStatusRequest };
1852
+ export { ControlStream, type EarlyDiscardPolicyConfig, type FetchOptions, FetchPublication, FetchRequest, HybridTrackSource, type LiveObjectSource, LiveTrackSource, MOQtailClient, type MOQtailClientOptions, type MOQtailRequest, MemoryObjectCache, type ObjectCache, type PastObjectSource, PublishNamespaceRequest, PublishPublication, PublishRequest, RecvDatagramStream, RecvStream, RingBufferObjectCache, SendDatagramStream, SendStream, StaticTrackSource, SubscribeNamespaceRequest, type SubscribeOptions, SubscribePublication, SubscribeRequest, type SubscribeUpdateOptions, type SwitchOptions, type Track, type TrackSource, TrackStatusRequest };
package/dist/client.d.ts CHANGED
@@ -494,6 +494,7 @@ declare class SubscribeRequest implements PromiseLike<SubscribeOk | RequestError
494
494
  priority: number;
495
495
  forward: boolean;
496
496
  subscribeParameters: MessageParameter[];
497
+ earlyDiscardPolicy: EarlyDiscardPolicyConfig | undefined;
497
498
  largestLocation: Location | undefined;
498
499
  streamsAccepted: bigint;
499
500
  expectedStreams: bigint | undefined;
@@ -716,6 +717,8 @@ type SubscribeOptions = {
716
717
  startLocation?: Location;
717
718
  /** Required for {@link FilterType.AbsoluteRange}; exclusive upper group boundary (coerced to bigint if number provided). */
718
719
  endGroup?: bigint | number;
720
+ /** Per-subscription early discard policy. Overrides the client-level default set via {@link MOQtailClient.setEarlyDiscardPolicy}. */
721
+ earlyDiscardPolicy?: EarlyDiscardPolicyConfig;
719
722
  };
720
723
  /**
721
724
  * Narrowing update constraints applied to an existing SUBSCRIBE via {@link MOQtailClient.subscribeUpdate}.
@@ -799,6 +802,13 @@ type SwitchOptions = {
799
802
  * })
800
803
  * ```
801
804
  */
805
+ /**
806
+ * Configuration for the early discard policy applied to incoming subgroup streams.
807
+ */
808
+ type EarlyDiscardPolicyConfig = {
809
+ /** Cancel a subgroup QUIC stream if it has not fully completed within this many milliseconds. */
810
+ subgroupReceiveTimeout: number;
811
+ };
802
812
  type FetchOptions = {
803
813
  /** Request priority (0 = highest, 255 = lowest). Rounded & clamped. */
804
814
  priority: number;
@@ -1277,6 +1287,25 @@ declare class MOQtailClient {
1277
1287
  * ```
1278
1288
  */
1279
1289
  sendDatagram(trackAlias: bigint, object: MoqtObject): Promise<void>;
1290
+ /**
1291
+ * Sets (or replaces) the client-level default early discard policy for incoming subgroup streams.
1292
+ *
1293
+ * When set, each incoming subgroup QUIC stream is given a deadline of `subgroupReceiveTimeout` ms to
1294
+ * complete. If the stream has not finished within that window it is cancelled — objects already
1295
+ * delivered to the subscription are kept, but no further objects arrive from that stream.
1296
+ *
1297
+ * This is a client-wide default. Individual subscriptions can override it via the `earlyDiscardPolicy`
1298
+ * field in {@link SubscribeOptions}, which takes precedence over this setting.
1299
+ *
1300
+ * The policy takes effect on the next stream accepted after this call. Passing a new config
1301
+ * replaces the previous one. Pass `undefined` to remove the default.
1302
+ *
1303
+ * @example
1304
+ * ```ts
1305
+ * client.setEarlyDiscardPolicy({ subgroupReceiveTimeout: 2000 })
1306
+ * ```
1307
+ */
1308
+ setEarlyDiscardPolicy(config: EarlyDiscardPolicyConfig | undefined): void;
1280
1309
  /**
1281
1310
  * Gracefully terminates this {@link MOQtailClient} session and releases underlying {@link https://developer.mozilla.org/docs/Web/API/WebTransport | WebTransport} resources.
1282
1311
  *
@@ -1820,4 +1849,4 @@ declare class RecvStream {
1820
1849
  static new(readStream: ReadableStream<Uint8Array>, partialDataTimeout?: number, onDataReceived?: (data: SubgroupObject | SubgroupHeader | FetchObject | FetchHeader) => void): Promise<RecvStream>;
1821
1850
  }
1822
1851
 
1823
- export { ControlStream, type FetchOptions, FetchPublication, FetchRequest, HybridTrackSource, type LiveObjectSource, LiveTrackSource, MOQtailClient, type MOQtailClientOptions, type MOQtailRequest, MemoryObjectCache, type ObjectCache, type PastObjectSource, PublishNamespaceRequest, PublishPublication, PublishRequest, RecvDatagramStream, RecvStream, RingBufferObjectCache, SendDatagramStream, SendStream, StaticTrackSource, SubscribeNamespaceRequest, type SubscribeOptions, SubscribePublication, SubscribeRequest, type SubscribeUpdateOptions, type SwitchOptions, type Track, type TrackSource, TrackStatusRequest };
1852
+ export { ControlStream, type EarlyDiscardPolicyConfig, type FetchOptions, FetchPublication, FetchRequest, HybridTrackSource, type LiveObjectSource, LiveTrackSource, MOQtailClient, type MOQtailClientOptions, type MOQtailRequest, MemoryObjectCache, type ObjectCache, type PastObjectSource, PublishNamespaceRequest, PublishPublication, PublishRequest, RecvDatagramStream, RecvStream, RingBufferObjectCache, SendDatagramStream, SendStream, StaticTrackSource, SubscribeNamespaceRequest, type SubscribeOptions, SubscribePublication, SubscribeRequest, type SubscribeUpdateOptions, type SwitchOptions, type Track, type TrackSource, TrackStatusRequest };
package/dist/client.js CHANGED
@@ -5121,6 +5121,7 @@ var SubscribeRequest = class {
5121
5121
  priority;
5122
5122
  forward;
5123
5123
  subscribeParameters;
5124
+ earlyDiscardPolicy;
5124
5125
  largestLocation;
5125
5126
  // Updated on each received object
5126
5127
  streamsAccepted = 0n;
@@ -5956,6 +5957,11 @@ var handlerRequestsBlocked = async (_client, msg) => {
5956
5957
  logger.debug("handler/requests_blocked", "not implemented", msg);
5957
5958
  };
5958
5959
 
5960
+ // src/client/util/validators.ts
5961
+ function isValidTrackAlias(trackAlias) {
5962
+ return trackAlias !== void 0 && trackAlias >= 0n;
5963
+ }
5964
+
5959
5965
  // src/client/handler/subscribe.ts
5960
5966
  var handlerSubscribe = async (client, msg) => {
5961
5967
  logger.debug("handler/subscribe", `received requestId=${msg.requestId} ftn="${msg.fullTrackName}"`);
@@ -5988,7 +5994,7 @@ var handlerSubscribe = async (client, msg) => {
5988
5994
  await client.controlStream.send(response);
5989
5995
  return;
5990
5996
  }
5991
- if (!track.trackAlias) throw new Error("Expected track alias to be set");
5997
+ if (!isValidTrackAlias(track.trackAlias)) throw new Error("Expected track alias to be set");
5992
5998
  const largestLocation = track.trackSource.live.largestLocation;
5993
5999
  const parameters = [...msg.parameters];
5994
6000
  if (largestLocation) {
@@ -6454,6 +6460,8 @@ var MOQtailClient = class _MOQtailClient {
6454
6460
  #isDestroyed = false;
6455
6461
  /** Internal monotonically increasing client-assigned request id counter (even/odd parity scheme advances by 2). */
6456
6462
  #dontUseRequestId = 0n;
6463
+ /** Active early discard policy; undefined means no per-stream deadline is applied. */
6464
+ #earlyDiscardPolicy;
6457
6465
  /**
6458
6466
  * TODO: onNamespaceAnnounced may be a better name
6459
6467
  * Fired when an PUBLISH_NAMESPACE control message is processed for a track namespace.
@@ -6905,6 +6913,28 @@ var MOQtailClient = class _MOQtailClient {
6905
6913
  return FullTrackName.tryNew("unknown", `track-${trackAlias}`);
6906
6914
  }
6907
6915
  }
6916
+ /**
6917
+ * Sets (or replaces) the client-level default early discard policy for incoming subgroup streams.
6918
+ *
6919
+ * When set, each incoming subgroup QUIC stream is given a deadline of `subgroupReceiveTimeout` ms to
6920
+ * complete. If the stream has not finished within that window it is cancelled — objects already
6921
+ * delivered to the subscription are kept, but no further objects arrive from that stream.
6922
+ *
6923
+ * This is a client-wide default. Individual subscriptions can override it via the `earlyDiscardPolicy`
6924
+ * field in {@link SubscribeOptions}, which takes precedence over this setting.
6925
+ *
6926
+ * The policy takes effect on the next stream accepted after this call. Passing a new config
6927
+ * replaces the previous one. Pass `undefined` to remove the default.
6928
+ *
6929
+ * @example
6930
+ * ```ts
6931
+ * client.setEarlyDiscardPolicy({ subgroupReceiveTimeout: 2000 })
6932
+ * ```
6933
+ */
6934
+ setEarlyDiscardPolicy(config) {
6935
+ this.#ensureActive();
6936
+ this.#earlyDiscardPolicy = config;
6937
+ }
6908
6938
  /**
6909
6939
  * Gracefully terminates this {@link MOQtailClient} session and releases underlying {@link https://developer.mozilla.org/docs/Web/API/WebTransport | WebTransport} resources.
6910
6940
  *
@@ -6989,7 +7019,7 @@ var MOQtailClient = class _MOQtailClient {
6989
7019
  */
6990
7020
  addOrUpdateTrack(track) {
6991
7021
  this.#ensureActive();
6992
- if (!track.trackAlias) {
7022
+ if (!isValidTrackAlias(track.trackAlias)) {
6993
7023
  track.trackAlias = random60bitId();
6994
7024
  }
6995
7025
  this.trackSources.set(track.fullTrackName.toString(), track);
@@ -7121,6 +7151,7 @@ var MOQtailClient = class _MOQtailClient {
7121
7151
  break;
7122
7152
  }
7123
7153
  const request = new SubscribeRequest(msg);
7154
+ request.earlyDiscardPolicy = args.earlyDiscardPolicy;
7124
7155
  this.requests.set(request.requestId, request);
7125
7156
  this.requestIdMap.addMapping(request.requestId, request.fullTrackName);
7126
7157
  logger.debug("MOQtailClient", `subscribe: sending SUBSCRIBE requestId=${msg.requestId} ftn="${fullTrackName}"`);
@@ -7269,7 +7300,7 @@ var MOQtailClient = class _MOQtailClient {
7269
7300
  const request = this.requests.get(subscriptionRequestId);
7270
7301
  if (request instanceof SubscribeRequest) {
7271
7302
  const trackAlias = this.subscriptionAliasMap.get(subscriptionRequestId);
7272
- if (!trackAlias)
7303
+ if (!isValidTrackAlias(trackAlias))
7273
7304
  throw new InternalError("MOQtailClient.subscribeUpdate", "Request exists but track alias mapping does not");
7274
7305
  const subscription = this.subscriptions.get(trackAlias);
7275
7306
  if (!subscription)
@@ -7322,7 +7353,7 @@ var MOQtailClient = class _MOQtailClient {
7322
7353
  if (!(request instanceof SubscribeRequest))
7323
7354
  throw new ProtocolViolationError("MOQtailClient.switch", "Request id is not a subscription");
7324
7355
  const trackAlias = this.subscriptionAliasMap.get(subscriptionRequestId);
7325
- if (!trackAlias)
7356
+ if (!isValidTrackAlias(trackAlias))
7326
7357
  throw new InternalError("MOQtailClient.switch", "Request exists but track alias mapping does not");
7327
7358
  const subscription = this.subscriptions.get(trackAlias);
7328
7359
  if (!subscription) throw new InternalError("MOQtailClient.switch", "Request exists but subscription does not");
@@ -7986,43 +8017,55 @@ var MOQtailClient = class _MOQtailClient {
7986
8017
  if (subscription) {
7987
8018
  subscription.streamsAccepted++;
7988
8019
  let firstObjectId = null;
7989
- while (true) {
7990
- const { done, value: nextObject } = await reader.read();
7991
- if (done) {
7992
- break;
7993
- }
7994
- if (nextObject) {
7995
- if (nextObject instanceof SubgroupObject) {
7996
- if (!firstObjectId) {
7997
- firstObjectId = nextObject.objectId;
7998
- }
7999
- let subgroupId = null;
8000
- if (SubgroupHeaderType.isSubgroupIdZero(header.type)) {
8001
- subgroupId = 0n;
8002
- } else if (SubgroupHeaderType.isSubgroupIdFirstObjectId(header.type)) {
8003
- subgroupId = firstObjectId ?? null;
8004
- } else if (SubgroupHeaderType.hasExplicitSubgroupId(header.type)) {
8005
- subgroupId = header.subgroupId ?? null;
8006
- }
8007
- const fullTrackName = this.aliasFullTrackNameMap.get(header.trackAlias);
8008
- if (!fullTrackName) {
8009
- throw new ProtocolViolationError("MOQtailClient", "No full track name for received track alias");
8020
+ let subgroupTimeoutId;
8021
+ const effectiveDiscardPolicy = subscription.earlyDiscardPolicy ?? this.#earlyDiscardPolicy;
8022
+ if (effectiveDiscardPolicy?.subgroupReceiveTimeout !== void 0) {
8023
+ subgroupTimeoutId = setTimeout(() => {
8024
+ reader.cancel("early discard: subgroupReceiveTimeout exceeded").catch(() => {
8025
+ });
8026
+ }, effectiveDiscardPolicy.subgroupReceiveTimeout);
8027
+ }
8028
+ try {
8029
+ while (true) {
8030
+ const { done, value: nextObject } = await reader.read();
8031
+ if (done) {
8032
+ break;
8033
+ }
8034
+ if (nextObject) {
8035
+ if (nextObject instanceof SubgroupObject) {
8036
+ if (!firstObjectId) {
8037
+ firstObjectId = nextObject.objectId;
8038
+ }
8039
+ let subgroupId = null;
8040
+ if (SubgroupHeaderType.isSubgroupIdZero(header.type)) {
8041
+ subgroupId = 0n;
8042
+ } else if (SubgroupHeaderType.isSubgroupIdFirstObjectId(header.type)) {
8043
+ subgroupId = firstObjectId ?? null;
8044
+ } else if (SubgroupHeaderType.hasExplicitSubgroupId(header.type)) {
8045
+ subgroupId = header.subgroupId ?? null;
8046
+ }
8047
+ const fullTrackName = this.aliasFullTrackNameMap.get(header.trackAlias);
8048
+ if (!fullTrackName) {
8049
+ throw new ProtocolViolationError("MOQtailClient", "No full track name for received track alias");
8050
+ }
8051
+ const moqtObject = MoqtObject.fromSubgroupObject(
8052
+ nextObject,
8053
+ header.groupId,
8054
+ header.publisherPriority,
8055
+ subgroupId,
8056
+ fullTrackName
8057
+ );
8058
+ if (!subscription.largestLocation) subscription.largestLocation = moqtObject.location;
8059
+ if (subscription.largestLocation.compare(moqtObject.location) == -1)
8060
+ subscription.largestLocation = moqtObject.location;
8061
+ subscription.controller?.enqueue(moqtObject);
8062
+ continue;
8010
8063
  }
8011
- const moqtObject = MoqtObject.fromSubgroupObject(
8012
- nextObject,
8013
- header.groupId,
8014
- header.publisherPriority,
8015
- subgroupId,
8016
- fullTrackName
8017
- );
8018
- if (!subscription.largestLocation) subscription.largestLocation = moqtObject.location;
8019
- if (subscription.largestLocation.compare(moqtObject.location) == -1)
8020
- subscription.largestLocation = moqtObject.location;
8021
- subscription.controller?.enqueue(moqtObject);
8022
- continue;
8064
+ throw new ProtocolViolationError("MOQtailClient", "Received fetch object after subgroup header");
8023
8065
  }
8024
- throw new ProtocolViolationError("MOQtailClient", "Received fetch object after subgroup header");
8025
8066
  }
8067
+ } finally {
8068
+ if (subgroupTimeoutId !== void 0) clearTimeout(subgroupTimeoutId);
8026
8069
  }
8027
8070
  if (subscription.expectedStreams && subscription.expectedStreams === subscription.streamsAccepted) {
8028
8071
  subscription.controller?.close();
package/dist/index.cjs CHANGED
@@ -5617,6 +5617,7 @@ var SubscribeRequest = class {
5617
5617
  priority;
5618
5618
  forward;
5619
5619
  subscribeParameters;
5620
+ earlyDiscardPolicy;
5620
5621
  largestLocation;
5621
5622
  // Updated on each received object
5622
5623
  streamsAccepted = 0n;
@@ -6452,6 +6453,11 @@ var handlerRequestsBlocked = async (_client, msg) => {
6452
6453
  logger.debug("handler/requests_blocked", "not implemented", msg);
6453
6454
  };
6454
6455
 
6456
+ // src/client/util/validators.ts
6457
+ function isValidTrackAlias(trackAlias) {
6458
+ return trackAlias !== void 0 && trackAlias >= 0n;
6459
+ }
6460
+
6455
6461
  // src/client/handler/subscribe.ts
6456
6462
  var handlerSubscribe = async (client, msg) => {
6457
6463
  logger.debug("handler/subscribe", `received requestId=${msg.requestId} ftn="${msg.fullTrackName}"`);
@@ -6484,7 +6490,7 @@ var handlerSubscribe = async (client, msg) => {
6484
6490
  await client.controlStream.send(response);
6485
6491
  return;
6486
6492
  }
6487
- if (!track.trackAlias) throw new Error("Expected track alias to be set");
6493
+ if (!isValidTrackAlias(track.trackAlias)) throw new Error("Expected track alias to be set");
6488
6494
  const largestLocation = track.trackSource.live.largestLocation;
6489
6495
  const parameters = [...msg.parameters];
6490
6496
  if (largestLocation) {
@@ -6950,6 +6956,8 @@ var MOQtailClient = class _MOQtailClient {
6950
6956
  #isDestroyed = false;
6951
6957
  /** Internal monotonically increasing client-assigned request id counter (even/odd parity scheme advances by 2). */
6952
6958
  #dontUseRequestId = 0n;
6959
+ /** Active early discard policy; undefined means no per-stream deadline is applied. */
6960
+ #earlyDiscardPolicy;
6953
6961
  /**
6954
6962
  * TODO: onNamespaceAnnounced may be a better name
6955
6963
  * Fired when an PUBLISH_NAMESPACE control message is processed for a track namespace.
@@ -7401,6 +7409,28 @@ var MOQtailClient = class _MOQtailClient {
7401
7409
  return FullTrackName.tryNew("unknown", `track-${trackAlias}`);
7402
7410
  }
7403
7411
  }
7412
+ /**
7413
+ * Sets (or replaces) the client-level default early discard policy for incoming subgroup streams.
7414
+ *
7415
+ * When set, each incoming subgroup QUIC stream is given a deadline of `subgroupReceiveTimeout` ms to
7416
+ * complete. If the stream has not finished within that window it is cancelled — objects already
7417
+ * delivered to the subscription are kept, but no further objects arrive from that stream.
7418
+ *
7419
+ * This is a client-wide default. Individual subscriptions can override it via the `earlyDiscardPolicy`
7420
+ * field in {@link SubscribeOptions}, which takes precedence over this setting.
7421
+ *
7422
+ * The policy takes effect on the next stream accepted after this call. Passing a new config
7423
+ * replaces the previous one. Pass `undefined` to remove the default.
7424
+ *
7425
+ * @example
7426
+ * ```ts
7427
+ * client.setEarlyDiscardPolicy({ subgroupReceiveTimeout: 2000 })
7428
+ * ```
7429
+ */
7430
+ setEarlyDiscardPolicy(config) {
7431
+ this.#ensureActive();
7432
+ this.#earlyDiscardPolicy = config;
7433
+ }
7404
7434
  /**
7405
7435
  * Gracefully terminates this {@link MOQtailClient} session and releases underlying {@link https://developer.mozilla.org/docs/Web/API/WebTransport | WebTransport} resources.
7406
7436
  *
@@ -7485,7 +7515,7 @@ var MOQtailClient = class _MOQtailClient {
7485
7515
  */
7486
7516
  addOrUpdateTrack(track) {
7487
7517
  this.#ensureActive();
7488
- if (!track.trackAlias) {
7518
+ if (!isValidTrackAlias(track.trackAlias)) {
7489
7519
  track.trackAlias = random60bitId();
7490
7520
  }
7491
7521
  this.trackSources.set(track.fullTrackName.toString(), track);
@@ -7617,6 +7647,7 @@ var MOQtailClient = class _MOQtailClient {
7617
7647
  break;
7618
7648
  }
7619
7649
  const request = new SubscribeRequest(msg);
7650
+ request.earlyDiscardPolicy = args.earlyDiscardPolicy;
7620
7651
  this.requests.set(request.requestId, request);
7621
7652
  this.requestIdMap.addMapping(request.requestId, request.fullTrackName);
7622
7653
  logger.debug("MOQtailClient", `subscribe: sending SUBSCRIBE requestId=${msg.requestId} ftn="${fullTrackName}"`);
@@ -7765,7 +7796,7 @@ var MOQtailClient = class _MOQtailClient {
7765
7796
  const request = this.requests.get(subscriptionRequestId);
7766
7797
  if (request instanceof SubscribeRequest) {
7767
7798
  const trackAlias = this.subscriptionAliasMap.get(subscriptionRequestId);
7768
- if (!trackAlias)
7799
+ if (!isValidTrackAlias(trackAlias))
7769
7800
  throw new InternalError("MOQtailClient.subscribeUpdate", "Request exists but track alias mapping does not");
7770
7801
  const subscription = this.subscriptions.get(trackAlias);
7771
7802
  if (!subscription)
@@ -7818,7 +7849,7 @@ var MOQtailClient = class _MOQtailClient {
7818
7849
  if (!(request instanceof SubscribeRequest))
7819
7850
  throw new ProtocolViolationError("MOQtailClient.switch", "Request id is not a subscription");
7820
7851
  const trackAlias = this.subscriptionAliasMap.get(subscriptionRequestId);
7821
- if (!trackAlias)
7852
+ if (!isValidTrackAlias(trackAlias))
7822
7853
  throw new InternalError("MOQtailClient.switch", "Request exists but track alias mapping does not");
7823
7854
  const subscription = this.subscriptions.get(trackAlias);
7824
7855
  if (!subscription) throw new InternalError("MOQtailClient.switch", "Request exists but subscription does not");
@@ -8482,43 +8513,55 @@ var MOQtailClient = class _MOQtailClient {
8482
8513
  if (subscription) {
8483
8514
  subscription.streamsAccepted++;
8484
8515
  let firstObjectId = null;
8485
- while (true) {
8486
- const { done, value: nextObject } = await reader.read();
8487
- if (done) {
8488
- break;
8489
- }
8490
- if (nextObject) {
8491
- if (nextObject instanceof SubgroupObject) {
8492
- if (!firstObjectId) {
8493
- firstObjectId = nextObject.objectId;
8494
- }
8495
- let subgroupId = null;
8496
- if (exports.SubgroupHeaderType.isSubgroupIdZero(header.type)) {
8497
- subgroupId = 0n;
8498
- } else if (exports.SubgroupHeaderType.isSubgroupIdFirstObjectId(header.type)) {
8499
- subgroupId = firstObjectId ?? null;
8500
- } else if (exports.SubgroupHeaderType.hasExplicitSubgroupId(header.type)) {
8501
- subgroupId = header.subgroupId ?? null;
8502
- }
8503
- const fullTrackName = this.aliasFullTrackNameMap.get(header.trackAlias);
8504
- if (!fullTrackName) {
8505
- throw new ProtocolViolationError("MOQtailClient", "No full track name for received track alias");
8516
+ let subgroupTimeoutId;
8517
+ const effectiveDiscardPolicy = subscription.earlyDiscardPolicy ?? this.#earlyDiscardPolicy;
8518
+ if (effectiveDiscardPolicy?.subgroupReceiveTimeout !== void 0) {
8519
+ subgroupTimeoutId = setTimeout(() => {
8520
+ reader.cancel("early discard: subgroupReceiveTimeout exceeded").catch(() => {
8521
+ });
8522
+ }, effectiveDiscardPolicy.subgroupReceiveTimeout);
8523
+ }
8524
+ try {
8525
+ while (true) {
8526
+ const { done, value: nextObject } = await reader.read();
8527
+ if (done) {
8528
+ break;
8529
+ }
8530
+ if (nextObject) {
8531
+ if (nextObject instanceof SubgroupObject) {
8532
+ if (!firstObjectId) {
8533
+ firstObjectId = nextObject.objectId;
8534
+ }
8535
+ let subgroupId = null;
8536
+ if (exports.SubgroupHeaderType.isSubgroupIdZero(header.type)) {
8537
+ subgroupId = 0n;
8538
+ } else if (exports.SubgroupHeaderType.isSubgroupIdFirstObjectId(header.type)) {
8539
+ subgroupId = firstObjectId ?? null;
8540
+ } else if (exports.SubgroupHeaderType.hasExplicitSubgroupId(header.type)) {
8541
+ subgroupId = header.subgroupId ?? null;
8542
+ }
8543
+ const fullTrackName = this.aliasFullTrackNameMap.get(header.trackAlias);
8544
+ if (!fullTrackName) {
8545
+ throw new ProtocolViolationError("MOQtailClient", "No full track name for received track alias");
8546
+ }
8547
+ const moqtObject = MoqtObject.fromSubgroupObject(
8548
+ nextObject,
8549
+ header.groupId,
8550
+ header.publisherPriority,
8551
+ subgroupId,
8552
+ fullTrackName
8553
+ );
8554
+ if (!subscription.largestLocation) subscription.largestLocation = moqtObject.location;
8555
+ if (subscription.largestLocation.compare(moqtObject.location) == -1)
8556
+ subscription.largestLocation = moqtObject.location;
8557
+ subscription.controller?.enqueue(moqtObject);
8558
+ continue;
8506
8559
  }
8507
- const moqtObject = MoqtObject.fromSubgroupObject(
8508
- nextObject,
8509
- header.groupId,
8510
- header.publisherPriority,
8511
- subgroupId,
8512
- fullTrackName
8513
- );
8514
- if (!subscription.largestLocation) subscription.largestLocation = moqtObject.location;
8515
- if (subscription.largestLocation.compare(moqtObject.location) == -1)
8516
- subscription.largestLocation = moqtObject.location;
8517
- subscription.controller?.enqueue(moqtObject);
8518
- continue;
8560
+ throw new ProtocolViolationError("MOQtailClient", "Received fetch object after subgroup header");
8519
8561
  }
8520
- throw new ProtocolViolationError("MOQtailClient", "Received fetch object after subgroup header");
8521
8562
  }
8563
+ } finally {
8564
+ if (subgroupTimeoutId !== void 0) clearTimeout(subgroupTimeoutId);
8522
8565
  }
8523
8566
  if (subscription.expectedStreams && subscription.expectedStreams === subscription.streamsAccepted) {
8524
8567
  subscription.controller?.close();
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { ControlStream, FetchOptions, FetchPublication, FetchRequest, HybridTrackSource, LiveObjectSource, LiveTrackSource, MOQtailClient, MOQtailClientOptions, MOQtailRequest, MemoryObjectCache, ObjectCache, PastObjectSource, PublishNamespaceRequest, PublishPublication, PublishRequest, RecvDatagramStream, RecvStream, RingBufferObjectCache, SendDatagramStream, SendStream, StaticTrackSource, SubscribeNamespaceRequest, SubscribeOptions, SubscribePublication, SubscribeRequest, SubscribeUpdateOptions, SwitchOptions, Track, TrackSource, TrackStatusRequest } from './client.cjs';
1
+ export { ControlStream, EarlyDiscardPolicyConfig, FetchOptions, FetchPublication, FetchRequest, HybridTrackSource, LiveObjectSource, LiveTrackSource, MOQtailClient, MOQtailClientOptions, MOQtailRequest, MemoryObjectCache, ObjectCache, PastObjectSource, PublishNamespaceRequest, PublishPublication, PublishRequest, RecvDatagramStream, RecvStream, RingBufferObjectCache, SendDatagramStream, SendStream, StaticTrackSource, SubscribeNamespaceRequest, SubscribeOptions, SubscribePublication, SubscribeRequest, SubscribeUpdateOptions, SwitchOptions, Track, TrackSource, TrackStatusRequest } from './client.cjs';
2
2
  export { AudioLevel, CMSF, CMSFCatalog, CMSFTrack, CaptureTimestamp, CastingError, ExtensionHeader, ExtensionHeaders, ImmutableExtensionsObjectExtension, InternalError, InvalidTypeError, InvalidUTF8Error, KeyValueFormattingError, LengthExceedsMaxError, MOQtailError, NotEnoughBytesError, ObjectExtension, PriorGroupIdGapExtension, PriorObjectIdGapExtension, ProtocolViolationError, RequestIdError, Switch, TerminationCode, TerminationError, TimeoutError, TrackNameError, UnknownObjectExtension, VarIntOverflowError, VideoConfig, VideoFrameMarking } from './model.cjs';
3
3
  export { A as AuthTokenVariant, a as AuthorizationToken, B as BaseByteBuffer, b as ByteBuffer, C as ClientSetup, c as ControlMessage, d as ControlMessageType, D as Datagram, e as DefaultPublisherGroupOrderExtension, f as DefaultPublisherPriorityExtension, g as DeliveryTimeout, h as DeliveryTimeoutExtension, i as DynamicGroupsExtension, E as EndOfRangeKind, j as Expires, F as Fetch, k as FetchCancel, l as FetchHeader, m as FetchHeaderType, n as FetchObject, o as FetchObjectContext, p as FetchOk, q as FetchType, r as FilterType, s as Forward, t as FrozenByteBuffer, u as FullTrackName, G as GoAway, v as GroupOrder, w as GroupOrderParam, H as Header, I as ImmutableExtensionsExtension, K as KeyValuePair, L as LOCHeaderExtensionId, x as LargestObject, y as Location, M as MAX_FULL_TRACK_NAME_LENGTH, z as MAX_NAMESPACE_TUPLE_COUNT, J as MAX_REASON_PHRASE_LEN, N as MaxAuthTokenCacheSize, O as MaxCacheDurationExtension, P as MaxRequestId, Q as MaxRequestIdParameter, R as MessageParameter, S as MessageParameterType, T as MessageParameters, U as MoqtObject, V as Namespace, W as NamespaceDone, X as NamespaceSubscribeOptions, Y as NewGroupRequest, Z as ObjectDatagramType, _ as ObjectForwardingPreference, $ as ObjectStatus, a0 as Parameter, a1 as Path, a2 as Publish, a3 as PublishDone, a4 as PublishDoneStatusCode, a5 as PublishNamespace, a6 as PublishNamespaceCancel, a7 as PublishNamespaceDone, a8 as PublishOk, a9 as ReasonPhrase, aa as RequestError, ab as RequestErrorCode, ac as RequestIdMap, ad as RequestOk, ae as RequestUpdate, af as RequestsBlocked, ag as SUPPORTED_VERSIONS, ah as ServerSetup, ai as SetupParameter, aj as SetupParameterType, ak as SetupParameters, al as SubgroupHeader, am as SubgroupHeaderType, an as SubgroupObject, ao as Subscribe, ap as SubscribeNamespace, aq as SubscribeOk, ar as SubscriberPriority, as as SubscriptionFilter, at as TokenAliasType, au as TrackExtension, av as TrackExtensionType, aw as TrackStatus, ax as TrackStatusCode, ay as Tuple, az as TupleField, aA as UnknownTrackExtension, aB as Unsubscribe, aC as UnsubscribeNamespace, aD as applyMessageParameterUpdate, aE as controlMessageTypeFromBigInt, aF as fetchTypeFromBigInt, aG as filterTypeFromBigInt, aH as groupOrderFromNumber, aI as isBytes, aJ as isVarInt, aK as locHeaderExtensionIdFromNumber, aL as messageParameterTypeFromNumber, aM as publishDoneStatusCodeFromBigInt, aN as requestErrorCodeFromBigInt, aO as setupParameterTypeFromNumber, aP as tokenAliasTypeFromNumber, aQ as trackStatusCodeFromBigInt } from './setup_parameter-BOeGq6Mv.cjs';
4
4
  export { ClockNormalizer, NetworkTelemetry } from './util.cjs';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { ControlStream, FetchOptions, FetchPublication, FetchRequest, HybridTrackSource, LiveObjectSource, LiveTrackSource, MOQtailClient, MOQtailClientOptions, MOQtailRequest, MemoryObjectCache, ObjectCache, PastObjectSource, PublishNamespaceRequest, PublishPublication, PublishRequest, RecvDatagramStream, RecvStream, RingBufferObjectCache, SendDatagramStream, SendStream, StaticTrackSource, SubscribeNamespaceRequest, SubscribeOptions, SubscribePublication, SubscribeRequest, SubscribeUpdateOptions, SwitchOptions, Track, TrackSource, TrackStatusRequest } from './client.js';
1
+ export { ControlStream, EarlyDiscardPolicyConfig, FetchOptions, FetchPublication, FetchRequest, HybridTrackSource, LiveObjectSource, LiveTrackSource, MOQtailClient, MOQtailClientOptions, MOQtailRequest, MemoryObjectCache, ObjectCache, PastObjectSource, PublishNamespaceRequest, PublishPublication, PublishRequest, RecvDatagramStream, RecvStream, RingBufferObjectCache, SendDatagramStream, SendStream, StaticTrackSource, SubscribeNamespaceRequest, SubscribeOptions, SubscribePublication, SubscribeRequest, SubscribeUpdateOptions, SwitchOptions, Track, TrackSource, TrackStatusRequest } from './client.js';
2
2
  export { AudioLevel, CMSF, CMSFCatalog, CMSFTrack, CaptureTimestamp, CastingError, ExtensionHeader, ExtensionHeaders, ImmutableExtensionsObjectExtension, InternalError, InvalidTypeError, InvalidUTF8Error, KeyValueFormattingError, LengthExceedsMaxError, MOQtailError, NotEnoughBytesError, ObjectExtension, PriorGroupIdGapExtension, PriorObjectIdGapExtension, ProtocolViolationError, RequestIdError, Switch, TerminationCode, TerminationError, TimeoutError, TrackNameError, UnknownObjectExtension, VarIntOverflowError, VideoConfig, VideoFrameMarking } from './model.js';
3
3
  export { A as AuthTokenVariant, a as AuthorizationToken, B as BaseByteBuffer, b as ByteBuffer, C as ClientSetup, c as ControlMessage, d as ControlMessageType, D as Datagram, e as DefaultPublisherGroupOrderExtension, f as DefaultPublisherPriorityExtension, g as DeliveryTimeout, h as DeliveryTimeoutExtension, i as DynamicGroupsExtension, E as EndOfRangeKind, j as Expires, F as Fetch, k as FetchCancel, l as FetchHeader, m as FetchHeaderType, n as FetchObject, o as FetchObjectContext, p as FetchOk, q as FetchType, r as FilterType, s as Forward, t as FrozenByteBuffer, u as FullTrackName, G as GoAway, v as GroupOrder, w as GroupOrderParam, H as Header, I as ImmutableExtensionsExtension, K as KeyValuePair, L as LOCHeaderExtensionId, x as LargestObject, y as Location, M as MAX_FULL_TRACK_NAME_LENGTH, z as MAX_NAMESPACE_TUPLE_COUNT, J as MAX_REASON_PHRASE_LEN, N as MaxAuthTokenCacheSize, O as MaxCacheDurationExtension, P as MaxRequestId, Q as MaxRequestIdParameter, R as MessageParameter, S as MessageParameterType, T as MessageParameters, U as MoqtObject, V as Namespace, W as NamespaceDone, X as NamespaceSubscribeOptions, Y as NewGroupRequest, Z as ObjectDatagramType, _ as ObjectForwardingPreference, $ as ObjectStatus, a0 as Parameter, a1 as Path, a2 as Publish, a3 as PublishDone, a4 as PublishDoneStatusCode, a5 as PublishNamespace, a6 as PublishNamespaceCancel, a7 as PublishNamespaceDone, a8 as PublishOk, a9 as ReasonPhrase, aa as RequestError, ab as RequestErrorCode, ac as RequestIdMap, ad as RequestOk, ae as RequestUpdate, af as RequestsBlocked, ag as SUPPORTED_VERSIONS, ah as ServerSetup, ai as SetupParameter, aj as SetupParameterType, ak as SetupParameters, al as SubgroupHeader, am as SubgroupHeaderType, an as SubgroupObject, ao as Subscribe, ap as SubscribeNamespace, aq as SubscribeOk, ar as SubscriberPriority, as as SubscriptionFilter, at as TokenAliasType, au as TrackExtension, av as TrackExtensionType, aw as TrackStatus, ax as TrackStatusCode, ay as Tuple, az as TupleField, aA as UnknownTrackExtension, aB as Unsubscribe, aC as UnsubscribeNamespace, aD as applyMessageParameterUpdate, aE as controlMessageTypeFromBigInt, aF as fetchTypeFromBigInt, aG as filterTypeFromBigInt, aH as groupOrderFromNumber, aI as isBytes, aJ as isVarInt, aK as locHeaderExtensionIdFromNumber, aL as messageParameterTypeFromNumber, aM as publishDoneStatusCodeFromBigInt, aN as requestErrorCodeFromBigInt, aO as setupParameterTypeFromNumber, aP as tokenAliasTypeFromNumber, aQ as trackStatusCodeFromBigInt } from './setup_parameter-BOeGq6Mv.js';
4
4
  export { ClockNormalizer, NetworkTelemetry } from './util.js';
package/dist/index.js CHANGED
@@ -5615,6 +5615,7 @@ var SubscribeRequest = class {
5615
5615
  priority;
5616
5616
  forward;
5617
5617
  subscribeParameters;
5618
+ earlyDiscardPolicy;
5618
5619
  largestLocation;
5619
5620
  // Updated on each received object
5620
5621
  streamsAccepted = 0n;
@@ -6450,6 +6451,11 @@ var handlerRequestsBlocked = async (_client, msg) => {
6450
6451
  logger.debug("handler/requests_blocked", "not implemented", msg);
6451
6452
  };
6452
6453
 
6454
+ // src/client/util/validators.ts
6455
+ function isValidTrackAlias(trackAlias) {
6456
+ return trackAlias !== void 0 && trackAlias >= 0n;
6457
+ }
6458
+
6453
6459
  // src/client/handler/subscribe.ts
6454
6460
  var handlerSubscribe = async (client, msg) => {
6455
6461
  logger.debug("handler/subscribe", `received requestId=${msg.requestId} ftn="${msg.fullTrackName}"`);
@@ -6482,7 +6488,7 @@ var handlerSubscribe = async (client, msg) => {
6482
6488
  await client.controlStream.send(response);
6483
6489
  return;
6484
6490
  }
6485
- if (!track.trackAlias) throw new Error("Expected track alias to be set");
6491
+ if (!isValidTrackAlias(track.trackAlias)) throw new Error("Expected track alias to be set");
6486
6492
  const largestLocation = track.trackSource.live.largestLocation;
6487
6493
  const parameters = [...msg.parameters];
6488
6494
  if (largestLocation) {
@@ -6948,6 +6954,8 @@ var MOQtailClient = class _MOQtailClient {
6948
6954
  #isDestroyed = false;
6949
6955
  /** Internal monotonically increasing client-assigned request id counter (even/odd parity scheme advances by 2). */
6950
6956
  #dontUseRequestId = 0n;
6957
+ /** Active early discard policy; undefined means no per-stream deadline is applied. */
6958
+ #earlyDiscardPolicy;
6951
6959
  /**
6952
6960
  * TODO: onNamespaceAnnounced may be a better name
6953
6961
  * Fired when an PUBLISH_NAMESPACE control message is processed for a track namespace.
@@ -7399,6 +7407,28 @@ var MOQtailClient = class _MOQtailClient {
7399
7407
  return FullTrackName.tryNew("unknown", `track-${trackAlias}`);
7400
7408
  }
7401
7409
  }
7410
+ /**
7411
+ * Sets (or replaces) the client-level default early discard policy for incoming subgroup streams.
7412
+ *
7413
+ * When set, each incoming subgroup QUIC stream is given a deadline of `subgroupReceiveTimeout` ms to
7414
+ * complete. If the stream has not finished within that window it is cancelled — objects already
7415
+ * delivered to the subscription are kept, but no further objects arrive from that stream.
7416
+ *
7417
+ * This is a client-wide default. Individual subscriptions can override it via the `earlyDiscardPolicy`
7418
+ * field in {@link SubscribeOptions}, which takes precedence over this setting.
7419
+ *
7420
+ * The policy takes effect on the next stream accepted after this call. Passing a new config
7421
+ * replaces the previous one. Pass `undefined` to remove the default.
7422
+ *
7423
+ * @example
7424
+ * ```ts
7425
+ * client.setEarlyDiscardPolicy({ subgroupReceiveTimeout: 2000 })
7426
+ * ```
7427
+ */
7428
+ setEarlyDiscardPolicy(config) {
7429
+ this.#ensureActive();
7430
+ this.#earlyDiscardPolicy = config;
7431
+ }
7402
7432
  /**
7403
7433
  * Gracefully terminates this {@link MOQtailClient} session and releases underlying {@link https://developer.mozilla.org/docs/Web/API/WebTransport | WebTransport} resources.
7404
7434
  *
@@ -7483,7 +7513,7 @@ var MOQtailClient = class _MOQtailClient {
7483
7513
  */
7484
7514
  addOrUpdateTrack(track) {
7485
7515
  this.#ensureActive();
7486
- if (!track.trackAlias) {
7516
+ if (!isValidTrackAlias(track.trackAlias)) {
7487
7517
  track.trackAlias = random60bitId();
7488
7518
  }
7489
7519
  this.trackSources.set(track.fullTrackName.toString(), track);
@@ -7615,6 +7645,7 @@ var MOQtailClient = class _MOQtailClient {
7615
7645
  break;
7616
7646
  }
7617
7647
  const request = new SubscribeRequest(msg);
7648
+ request.earlyDiscardPolicy = args.earlyDiscardPolicy;
7618
7649
  this.requests.set(request.requestId, request);
7619
7650
  this.requestIdMap.addMapping(request.requestId, request.fullTrackName);
7620
7651
  logger.debug("MOQtailClient", `subscribe: sending SUBSCRIBE requestId=${msg.requestId} ftn="${fullTrackName}"`);
@@ -7763,7 +7794,7 @@ var MOQtailClient = class _MOQtailClient {
7763
7794
  const request = this.requests.get(subscriptionRequestId);
7764
7795
  if (request instanceof SubscribeRequest) {
7765
7796
  const trackAlias = this.subscriptionAliasMap.get(subscriptionRequestId);
7766
- if (!trackAlias)
7797
+ if (!isValidTrackAlias(trackAlias))
7767
7798
  throw new InternalError("MOQtailClient.subscribeUpdate", "Request exists but track alias mapping does not");
7768
7799
  const subscription = this.subscriptions.get(trackAlias);
7769
7800
  if (!subscription)
@@ -7816,7 +7847,7 @@ var MOQtailClient = class _MOQtailClient {
7816
7847
  if (!(request instanceof SubscribeRequest))
7817
7848
  throw new ProtocolViolationError("MOQtailClient.switch", "Request id is not a subscription");
7818
7849
  const trackAlias = this.subscriptionAliasMap.get(subscriptionRequestId);
7819
- if (!trackAlias)
7850
+ if (!isValidTrackAlias(trackAlias))
7820
7851
  throw new InternalError("MOQtailClient.switch", "Request exists but track alias mapping does not");
7821
7852
  const subscription = this.subscriptions.get(trackAlias);
7822
7853
  if (!subscription) throw new InternalError("MOQtailClient.switch", "Request exists but subscription does not");
@@ -8480,43 +8511,55 @@ var MOQtailClient = class _MOQtailClient {
8480
8511
  if (subscription) {
8481
8512
  subscription.streamsAccepted++;
8482
8513
  let firstObjectId = null;
8483
- while (true) {
8484
- const { done, value: nextObject } = await reader.read();
8485
- if (done) {
8486
- break;
8487
- }
8488
- if (nextObject) {
8489
- if (nextObject instanceof SubgroupObject) {
8490
- if (!firstObjectId) {
8491
- firstObjectId = nextObject.objectId;
8492
- }
8493
- let subgroupId = null;
8494
- if (SubgroupHeaderType.isSubgroupIdZero(header.type)) {
8495
- subgroupId = 0n;
8496
- } else if (SubgroupHeaderType.isSubgroupIdFirstObjectId(header.type)) {
8497
- subgroupId = firstObjectId ?? null;
8498
- } else if (SubgroupHeaderType.hasExplicitSubgroupId(header.type)) {
8499
- subgroupId = header.subgroupId ?? null;
8500
- }
8501
- const fullTrackName = this.aliasFullTrackNameMap.get(header.trackAlias);
8502
- if (!fullTrackName) {
8503
- throw new ProtocolViolationError("MOQtailClient", "No full track name for received track alias");
8514
+ let subgroupTimeoutId;
8515
+ const effectiveDiscardPolicy = subscription.earlyDiscardPolicy ?? this.#earlyDiscardPolicy;
8516
+ if (effectiveDiscardPolicy?.subgroupReceiveTimeout !== void 0) {
8517
+ subgroupTimeoutId = setTimeout(() => {
8518
+ reader.cancel("early discard: subgroupReceiveTimeout exceeded").catch(() => {
8519
+ });
8520
+ }, effectiveDiscardPolicy.subgroupReceiveTimeout);
8521
+ }
8522
+ try {
8523
+ while (true) {
8524
+ const { done, value: nextObject } = await reader.read();
8525
+ if (done) {
8526
+ break;
8527
+ }
8528
+ if (nextObject) {
8529
+ if (nextObject instanceof SubgroupObject) {
8530
+ if (!firstObjectId) {
8531
+ firstObjectId = nextObject.objectId;
8532
+ }
8533
+ let subgroupId = null;
8534
+ if (SubgroupHeaderType.isSubgroupIdZero(header.type)) {
8535
+ subgroupId = 0n;
8536
+ } else if (SubgroupHeaderType.isSubgroupIdFirstObjectId(header.type)) {
8537
+ subgroupId = firstObjectId ?? null;
8538
+ } else if (SubgroupHeaderType.hasExplicitSubgroupId(header.type)) {
8539
+ subgroupId = header.subgroupId ?? null;
8540
+ }
8541
+ const fullTrackName = this.aliasFullTrackNameMap.get(header.trackAlias);
8542
+ if (!fullTrackName) {
8543
+ throw new ProtocolViolationError("MOQtailClient", "No full track name for received track alias");
8544
+ }
8545
+ const moqtObject = MoqtObject.fromSubgroupObject(
8546
+ nextObject,
8547
+ header.groupId,
8548
+ header.publisherPriority,
8549
+ subgroupId,
8550
+ fullTrackName
8551
+ );
8552
+ if (!subscription.largestLocation) subscription.largestLocation = moqtObject.location;
8553
+ if (subscription.largestLocation.compare(moqtObject.location) == -1)
8554
+ subscription.largestLocation = moqtObject.location;
8555
+ subscription.controller?.enqueue(moqtObject);
8556
+ continue;
8504
8557
  }
8505
- const moqtObject = MoqtObject.fromSubgroupObject(
8506
- nextObject,
8507
- header.groupId,
8508
- header.publisherPriority,
8509
- subgroupId,
8510
- fullTrackName
8511
- );
8512
- if (!subscription.largestLocation) subscription.largestLocation = moqtObject.location;
8513
- if (subscription.largestLocation.compare(moqtObject.location) == -1)
8514
- subscription.largestLocation = moqtObject.location;
8515
- subscription.controller?.enqueue(moqtObject);
8516
- continue;
8558
+ throw new ProtocolViolationError("MOQtailClient", "Received fetch object after subgroup header");
8517
8559
  }
8518
- throw new ProtocolViolationError("MOQtailClient", "Received fetch object after subgroup header");
8519
8560
  }
8561
+ } finally {
8562
+ if (subgroupTimeoutId !== void 0) clearTimeout(subgroupTimeoutId);
8520
8563
  }
8521
8564
  if (subscription.expectedStreams && subscription.expectedStreams === subscription.streamsAccepted) {
8522
8565
  subscription.controller?.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moqtail",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Media Over QUIC Transport client implementation",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",