@svta/cml-cmcd 2.5.0 → 2.6.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/index.js CHANGED
@@ -970,6 +970,33 @@ const CMCD_KEYS = /* @__PURE__ */ [
970
970
  */
971
971
  const CMCD_MIME_TYPE = "application/cmcd";
972
972
 
973
+ //#endregion
974
+ //#region src/CMCD_REQUEST_PROVENANCE.ts
975
+ /**
976
+ * The `customData` key under which `CmcdReporter.createRequestReport()`
977
+ * stamps session provenance on every request it returns.
978
+ *
979
+ * The value is a frozen `CmcdRequestProvenance` record: `sid` names the
980
+ * session that issued the request, `cid` is the content id in effect when
981
+ * the request was issued, and `data` carries the request's per-call data
982
+ * as an encoded CMCD string. `CmcdReporter.recordResponseReceived()` reads
983
+ * the record to attribute a late response to its session and to rebuild
984
+ * the request-time report keys.
985
+ * Spread and `Object.assign` carry the record through ordinary request
986
+ * clones, but `JSON.stringify` and structured clone drop symbol-keyed
987
+ * properties: read the value before such a boundary and restore it
988
+ * afterward. The record itself survives JSON, and attribution is by the
989
+ * `sid` value rather than object identity, so a revived plain-object copy
990
+ * attributes exactly. A record naming a `sid` the reporter does not retain
991
+ * drops the response rather than relabeling it.
992
+ *
993
+ * Backed by the symbol registry so duplicated copies of this library in
994
+ * one bundle interoperate. The registry key is stable across versions.
995
+ *
996
+ * @public
997
+ */
998
+ const CMCD_REQUEST_PROVENANCE = /* @__PURE__ */ Symbol.for("@svta/cml-cmcd/request-provenance");
999
+
973
1000
  //#endregion
974
1001
  //#region src/CMCD_V1.ts
975
1002
  /**
@@ -1473,6 +1500,50 @@ const CmcdTransmissionMode = {
1473
1500
  HEADERS: CMCD_HEADERS
1474
1501
  };
1475
1502
 
1503
+ //#endregion
1504
+ //#region src/upConvertToV2.ts
1505
+ /**
1506
+ * Up-convert V1 CMCD data to V2 format.
1507
+ *
1508
+ * - Wraps plain scalar values in arrays for inner-list keys.
1509
+ * - Wraps `nor` string in an array.
1510
+ *
1511
+ * If the data is already V2 (has `v: 2`), it is returned unchanged.
1512
+ *
1513
+ * @internal
1514
+ */
1515
+ function upConvertToV2(obj) {
1516
+ if (obj["v"] === CMCD_V2) return obj;
1517
+ const result = {};
1518
+ for (const [key, value] of Object.entries(obj)) {
1519
+ if (value == null) {
1520
+ result[key] = value;
1521
+ continue;
1522
+ }
1523
+ if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(value)) result[key] = [value];
1524
+ else if (key === "nor" && typeof value === "string") result[key] = [value];
1525
+ else result[key] = value;
1526
+ }
1527
+ return result;
1528
+ }
1529
+
1530
+ //#endregion
1531
+ //#region src/decodeCmcd.ts
1532
+ function reduceValue(value, useSymbol) {
1533
+ if (Array.isArray(value)) return value.map((item) => reduceValue(item, useSymbol));
1534
+ if (typeof value === "symbol") return useSymbol === void 0 ? symbolToStr(value) : value;
1535
+ if (value instanceof SfItem) return value.params ? new SfItem(reduceValue(value.value, useSymbol), value.params) : reduceValue(value.value, useSymbol);
1536
+ return value;
1537
+ }
1538
+ function decodeCmcd(cmcd, options) {
1539
+ if (!cmcd) return {};
1540
+ const sfDict = decodeSfDict(cmcd, options);
1541
+ const result = {};
1542
+ for (const [key, item] of Object.entries(sfDict)) result[key] = reduceValue(item, options?.useSymbol);
1543
+ if (options?.convertToLatest) return upConvertToV2(result);
1544
+ return result;
1545
+ }
1546
+
1476
1547
  //#endregion
1477
1548
  //#region src/CmcdReporter.ts
1478
1549
  function createEncodingOptions(reportingMode, config, baseUrl) {
@@ -1579,8 +1650,10 @@ function copyItemValue(value) {
1579
1650
  *
1580
1651
  * Complete for the CMCD value space rather than best-effort: `CmcdValue` and
1581
1652
  * `CmcdCustomValue` admit only primitives, `SfItem<primitive>`, and arrays of
1582
- * those, and `SfItem.params` is a flat record. Only called where a transform
1583
- * is configured.
1653
+ * those, and `SfItem.params` is a flat record. Called where a transform is
1654
+ * configured, on the ended session's store at archival (detaching the frozen
1655
+ * snapshot from caller-held references), and on the request's stored
1656
+ * player-facing view.
1584
1657
  */
1585
1658
  function copyReportValues(data) {
1586
1659
  const record = data;
@@ -1594,17 +1667,51 @@ function copyReportValues(data) {
1594
1667
  }
1595
1668
  return data;
1596
1669
  }
1670
+ /**
1671
+ * Decodes the per-call data snapshot a provenance record carries into
1672
+ * fresh, encodable report data: tokens revive as `SfToken` and
1673
+ * params-bearing items as `SfItem` (`useSymbol: false`), so every value
1674
+ * keeps its wire type through re-encoding. Only called in
1675
+ * `recordResponseReceived()`. Returns an empty object when the record is
1676
+ * absent, carries no snapshot, or the snapshot does not parse (a value
1677
+ * this reporter did not write); the response then reports its derived keys
1678
+ * over the session's data alone.
1679
+ */
1680
+ function decodeSnapshot(provenance) {
1681
+ const encoded = provenance !== null && typeof provenance === "object" ? provenance.data : void 0;
1682
+ if (typeof encoded !== "string" || !encoded) return {};
1683
+ try {
1684
+ return decodeCmcd(encoded, { useSymbol: false });
1685
+ } catch {
1686
+ return {};
1687
+ }
1688
+ }
1689
+ /**
1690
+ * Mints a session's frozen base provenance record: the issuing `sid`, and
1691
+ * the `cid` in effect at mint time. `update()` re-mints on every `cid`
1692
+ * change, so requests issued before a mid-session content change keep the
1693
+ * `cid` they were issued under while later requests carry the new one.
1694
+ */
1695
+ function mintProvenance(sid, cid) {
1696
+ return Object.freeze(typeof cid === "string" && cid ? {
1697
+ sid,
1698
+ cid
1699
+ } : { sid });
1700
+ }
1597
1701
  function defaultRequester(request) {
1598
1702
  const { url,...init } = request;
1599
1703
  return fetch(url, init);
1600
1704
  }
1601
1705
  function createCmcdReporterConfig(config) {
1602
1706
  const { version = CMCD_V2, eventTargets = [], sid = uuid(), transmissionMode = CMCD_QUERY,...rest } = config;
1707
+ const retention = config.sessionRetention;
1708
+ const sessionRetention = typeof retention === "number" ? Math.floor(retention) : NaN;
1603
1709
  return {
1604
1710
  ...rest,
1605
1711
  version,
1606
1712
  transmissionMode,
1607
1713
  sid,
1714
+ sessionRetention: sessionRetention >= 0 ? sessionRetention : 2,
1608
1715
  eventTargets: eventTargets.reduce((acc, target) => {
1609
1716
  if (target?.url && target.events?.length) acc.push({
1610
1717
  version: target.version || CMCD_V2,
@@ -1645,27 +1752,41 @@ var CmcdReporter = class {
1645
1752
  */
1646
1753
  constructor(config, requester = defaultRequester) {
1647
1754
  this.timeOrigin = performance.timeOrigin || performance.timing?.fetchStart || Date.now() - performance.now();
1648
- this.data = {};
1649
- this.msd = NaN;
1650
- this.eventTargets = /* @__PURE__ */ new Map();
1651
- this.lastEmitted = {};
1652
- this.requestTarget = {
1653
- sn: 0,
1654
- msdSent: false
1655
- };
1755
+ this.sessions = /* @__PURE__ */ new Map();
1756
+ this.intervals = /* @__PURE__ */ new Map();
1757
+ this.started = false;
1656
1758
  this.config = createCmcdReporterConfig(config);
1657
- this.data = {
1759
+ this.session = this.createSession(this.config.sid, {
1658
1760
  cid: this.config.cid,
1659
- sid: this.config.sid,
1660
1761
  v: this.config.version
1661
- };
1762
+ });
1763
+ this.sessions.set(this.session.sid, this.session);
1662
1764
  this.requester = requester;
1663
- for (const target of this.config.eventTargets) this.eventTargets.set(target, {
1664
- intervalId: void 0,
1765
+ }
1766
+ /**
1767
+ * Creates the state for a new session: fresh counters, gates, queues and
1768
+ * dedup baseline for every configured target.
1769
+ */
1770
+ createSession(sid, data) {
1771
+ const eventTargets = /* @__PURE__ */ new Map();
1772
+ for (const target of this.config.eventTargets) eventTargets.set(target, {
1665
1773
  sn: 0,
1666
1774
  msdSent: false,
1667
- queue: []
1775
+ queue: [],
1776
+ disposed: false
1668
1777
  });
1778
+ return {
1779
+ sid,
1780
+ provenance: mintProvenance(sid, data.cid),
1781
+ data,
1782
+ msd: NaN,
1783
+ lastEmitted: {},
1784
+ eventTargets,
1785
+ requestTarget: {
1786
+ sn: 0,
1787
+ msdSent: false
1788
+ }
1789
+ };
1669
1790
  }
1670
1791
  /**
1671
1792
  * Starts the CMCD reporter. Called by the player when the reporter is enabled.
@@ -1675,17 +1796,15 @@ var CmcdReporter = class {
1675
1796
  * populated before calling start().
1676
1797
  */
1677
1798
  start() {
1799
+ this.started = true;
1678
1800
  let failure;
1679
- this.eventTargets.forEach((target, config) => {
1680
- this.disarmInterval(target);
1681
- if (config.interval === 0 || !config.events.includes(CMCD_EVENT_TIME_INTERVAL)) return;
1682
- const timeIntervalEvent = () => {
1683
- this.recordTargetEvent(target, config, CMCD_EVENT_TIME_INTERVAL);
1684
- this.processEventTargets();
1685
- };
1686
- target.intervalId = setInterval(timeIntervalEvent, config.interval * 1e3);
1801
+ const session = this.session;
1802
+ session.eventTargets.forEach((target, config) => {
1803
+ this.disarmInterval(config);
1804
+ if (target.disposed || !this.armInterval(config)) return;
1687
1805
  try {
1688
- timeIntervalEvent();
1806
+ this.recordTargetEvent(session, target, config, CMCD_EVENT_TIME_INTERVAL);
1807
+ this.processEventTargets();
1689
1808
  } catch (error) {
1690
1809
  failure ??= { error };
1691
1810
  }
@@ -1693,15 +1812,31 @@ var CmcdReporter = class {
1693
1812
  if (failure) throw failure.error;
1694
1813
  }
1695
1814
  /**
1815
+ * Arms the time-interval timer for an event target when its config calls
1816
+ * for one. Returns whether a timer was armed. The callback reports into
1817
+ * whichever session is current when it fires.
1818
+ */
1819
+ armInterval(config) {
1820
+ if (config.interval === 0 || !config.events.includes(CMCD_EVENT_TIME_INTERVAL)) return false;
1821
+ this.intervals.set(config, setInterval(() => {
1822
+ const session = this.session;
1823
+ const target = session.eventTargets.get(config);
1824
+ if (target) {
1825
+ this.recordTargetEvent(session, target, config, CMCD_EVENT_TIME_INTERVAL);
1826
+ this.processEventTargets();
1827
+ }
1828
+ }, config.interval * 1e3));
1829
+ return true;
1830
+ }
1831
+ /**
1696
1832
  * Stops the CMCD reporter. Called by the player when the reporter is disabled.
1697
1833
  *
1698
1834
  * @param flush - Whether to flush the event targets.
1699
1835
  */
1700
1836
  stop(flush = false) {
1837
+ this.started = false;
1701
1838
  if (flush) this.flush();
1702
- this.eventTargets.forEach((target) => {
1703
- this.disarmInterval(target);
1704
- });
1839
+ for (const config of this.config.eventTargets) this.disarmInterval(config);
1705
1840
  }
1706
1841
  /**
1707
1842
  * Forces the sending of all event reports, regardless of the batch size or interval.
@@ -1739,17 +1874,63 @@ var CmcdReporter = class {
1739
1874
  *
1740
1875
  * A `sid` change resets the dedup baseline.
1741
1876
  *
1877
+ * `sid` and `msd` are session-owned: the reporter tracks them itself and
1878
+ * stamps them onto every outgoing report, so this method is the only way
1879
+ * to change them. A per-call value on {@link CmcdReporter.recordEvent},
1880
+ * {@link CmcdReporter.createRequestReport}, or
1881
+ * {@link CmcdReporter.recordResponseReceived} has no effect; response
1882
+ * attribution is by the provenance record alone.
1883
+ * `msd` must be a finite number of milliseconds between `0` and
1884
+ * `999_999_999_999_999` (the RFC 8941 integer maximum); it is rounded to
1885
+ * the nearest integer, and invalid values are ignored.
1886
+ *
1742
1887
  * @param data - The data to update.
1743
1888
  */
1744
1889
  update(data) {
1745
- if (data.sid && data.sid !== this.data.sid) this.resetSession();
1746
- if (data.msd && !isNaN(data.msd)) this.msd = data.msd;
1747
- this.data = {
1748
- ...this.data,
1890
+ if (data.sid && data.sid !== this.session.sid) this.startSession(data.sid);
1891
+ const session = this.session;
1892
+ const { msd } = data;
1893
+ if (typeof msd === "number" && Number.isFinite(msd) && msd >= 0) {
1894
+ const rounded = Math.round(msd);
1895
+ if (rounded <= 999999999999999) session.msd = rounded;
1896
+ }
1897
+ session.data = {
1898
+ ...session.data,
1749
1899
  ...data,
1900
+ sid: void 0,
1750
1901
  msd: void 0
1751
1902
  };
1752
- for (const entry of STATE_FIELDS) if (entry.field in data && !entry.equal(this.data[entry.field], this.lastEmitted[entry.field])) this.recordEvent(entry.event);
1903
+ if (session.data.cid !== session.provenance.cid) session.provenance = mintProvenance(session.sid, session.data.cid);
1904
+ for (const entry of STATE_FIELDS) if (entry.field in data && !entry.equal(session.data[entry.field], session.lastEmitted[entry.field])) this.recordEvent(entry.event);
1905
+ }
1906
+ /**
1907
+ * Starts a new session: the current one is archived in place for stale
1908
+ * attribution, a fresh one takes over, ended sessions are drained, and
1909
+ * the oldest are evicted beyond the configured retention.
1910
+ *
1911
+ * The data store is carried over, and the archived snapshot is frozen:
1912
+ * the ended session's value graph is detached first, so nested values
1913
+ * the caller handed to `update()` can no longer mutate it, and the new
1914
+ * session starts from a shallow copy of the detached graph, so `cid`,
1915
+ * `br`, custom keys and the rest survive a `sid` change as they always
1916
+ * have. Sessions are keyed by `sid`, which CTA-5004-B expects to be
1917
+ * unique per playback session: reusing one replaces the retained
1918
+ * namesake, whose remaining state dies as if evicted and whose late
1919
+ * responses relabel onto the replacement.
1920
+ */
1921
+ startSession(sid) {
1922
+ this.session.data = copyReportValues({ ...this.session.data });
1923
+ this.session = this.createSession(sid, { ...this.session.data });
1924
+ this.sessions.delete(sid);
1925
+ this.sessions.set(sid, this.session);
1926
+ this.processEventTargets();
1927
+ for (const key of this.sessions.keys()) {
1928
+ if (this.sessions.size <= this.config.sessionRetention + 1) break;
1929
+ this.sessions.delete(key);
1930
+ }
1931
+ if (this.started) {
1932
+ for (const config of this.config.eventTargets) if (!this.intervals.has(config)) this.armInterval(config);
1933
+ }
1753
1934
  }
1754
1935
  /**
1755
1936
  * Records an event. Called by the player when an event occurs.
@@ -1784,15 +1965,21 @@ var CmcdReporter = class {
1784
1965
  * @param data - Additional data to record with the event. This data
1785
1966
  * only applies to this event report, except for the dedup
1786
1967
  * field of a state-change event, which is also persisted
1787
- * into the reporter's data store.
1968
+ * into the reporter's data store. Session-owned keys
1969
+ * (`sid`, `msd`) supplied here are ignored; the reporter
1970
+ * stamps its own values.
1788
1971
  */
1789
1972
  recordEvent(type, data = {}) {
1790
- this.emitEvent(type, data);
1973
+ this.emitEvent(this.session, type, data);
1791
1974
  }
1792
1975
  /**
1793
- * Records an event across every configured target, applying
1976
+ * Records an event across every configured target of a session, applying
1794
1977
  * state-change dedup once before per-target fan-out.
1795
1978
  *
1979
+ * @param session - The session the event belongs to. Everything the event
1980
+ * touches (data store, dedup baseline, counters, queues)
1981
+ * is that session's; only `recordResponseReceived()` ever
1982
+ * passes an archived one.
1796
1983
  * @param type - The type of event to record.
1797
1984
  * @param data - Additional data to record with the event.
1798
1985
  * @param request - The media request that triggered the event, when
@@ -1800,21 +1987,21 @@ var CmcdReporter = class {
1800
1987
  * populates this; it is threaded through to each
1801
1988
  * target's `transform`.
1802
1989
  */
1803
- emitEvent(type, data, request) {
1990
+ emitEvent(session, type, data, request) {
1804
1991
  const entry = STATE_FIELDS_BY_EVENT.get(type);
1805
1992
  if (entry) {
1806
1993
  const field = entry.field;
1807
1994
  const incoming = data[field];
1808
- if (incoming !== void 0) Object.assign(this.data, { [field]: incoming });
1809
- const current = this.data[field];
1995
+ if (incoming !== void 0) Object.assign(session.data, { [field]: incoming });
1996
+ const current = session.data[field];
1810
1997
  if (current === void 0) return;
1811
- if (entry.equal(current, this.lastEmitted[field])) return;
1812
- Object.assign(this.lastEmitted, { [field]: entry.snapshot(current) });
1998
+ if (entry.equal(current, session.lastEmitted[field])) return;
1999
+ Object.assign(session.lastEmitted, { [field]: entry.snapshot(current) });
1813
2000
  }
1814
2001
  let failure;
1815
- this.eventTargets.forEach((target, config) => {
2002
+ session.eventTargets.forEach((target, config) => {
1816
2003
  try {
1817
- this.recordTargetEvent(target, config, type, data, request);
2004
+ this.recordTargetEvent(session, target, config, type, data, request);
1818
2005
  } catch (error) {
1819
2006
  failure ??= { error };
1820
2007
  }
@@ -1834,17 +2021,18 @@ var CmcdReporter = class {
1834
2021
  * @param request - The media request that triggered the event, when
1835
2022
  * one exists. Passed to the target's `transform`.
1836
2023
  */
1837
- recordTargetEvent(target, config, type, data = {}, request) {
1838
- if (!config.events.includes(type)) return;
2024
+ recordTargetEvent(session, target, config, type, data = {}, request) {
2025
+ if (target.disposed || !config.events.includes(type)) return;
1839
2026
  const item = {
1840
- ...this.data,
2027
+ ...session.data,
1841
2028
  ...data,
2029
+ sid: session.sid,
1842
2030
  e: type,
1843
2031
  ts: data.ts ?? Date.now()
1844
2032
  };
1845
2033
  const { transform } = config;
1846
2034
  if (!transform) {
1847
- this.queueTargetEvent(target, item, type);
2035
+ this.queueTargetEvent(session, target, config, item, type);
1848
2036
  return;
1849
2037
  }
1850
2038
  copyReportValues(item);
@@ -1855,27 +2043,33 @@ var CmcdReporter = class {
1855
2043
  if (report == null) return;
1856
2044
  if (!isUsableRequiredValue(report.ts)) report.ts = ts;
1857
2045
  if (requiredKey && isUsableRequiredValue(requiredValue) && !isUsableRequiredValue(report[requiredKey])) Object.assign(report, { [requiredKey]: requiredValue });
1858
- this.queueTargetEvent(target, report, type);
2046
+ this.queueTargetEvent(session, target, config, report, type);
1859
2047
  }
1860
2048
  /**
1861
- * Stamps the reporter-owned fields on a finished event report and pushes it
1862
- * to the target's queue.
2049
+ * Stamps the reporter-owned fields on a finished event report, encodes it,
2050
+ * and pushes the wire line to the target's queue.
1863
2051
  *
1864
2052
  * Called after any transform has run, so a transform cannot bypass the
1865
- * target's `events` filter via `e` or break `sn` continuity.
2053
+ * target's `events` filter via `e`, break `sn` continuity, or substitute
2054
+ * the session identity carried by `sid` and `msd`. Encoding here means a
2055
+ * report that cannot serialize throws inside the recording call that
2056
+ * produced it, instead of rejecting the batch send and re-queueing
2057
+ * forever.
1866
2058
  *
1867
2059
  * @param target - The target to queue the report for.
2060
+ * @param config - The configuration for the target.
1868
2061
  * @param report - The finished report data.
1869
2062
  * @param type - The type of event being reported.
1870
2063
  */
1871
- queueTargetEvent(target, report, type) {
2064
+ queueTargetEvent(session, target, config, report, type) {
1872
2065
  report.e = type;
1873
2066
  report.sn = target.sn++;
1874
- if (!isNaN(this.msd) && !target.msdSent) {
1875
- report.msd = this.msd;
2067
+ report.sid = session.sid;
2068
+ if (!isNaN(session.msd) && !target.msdSent && config.enabledKeys?.includes("msd")) {
2069
+ report.msd = session.msd;
1876
2070
  target.msdSent = true;
1877
- }
1878
- target.queue.push(report);
2071
+ } else delete report.msd;
2072
+ target.queue.push(encodeCmcd(report, createEncodingOptions(CMCD_EVENT_MODE, config)));
1879
2073
  }
1880
2074
  /**
1881
2075
  * Records a response-received event. Called by the player when a media
@@ -1894,26 +2088,55 @@ var CmcdReporter = class {
1894
2088
  *
1895
2089
  * The request's `customData` is generic, so a player can pass a request
1896
2090
  * carrying its own taxonomy (e.g. `{ requestType: 'segment' }`) without a
1897
- * cast. The reporter only reads the optional `cmcd` key that
1898
- * {@link CmcdReporter.createRequestReport} writes there; every other key is
1899
- * left untouched and stays visible to an event target's `transform` via
1900
- * its `request` argument.
2091
+ * cast. The reporter reads only the provenance record that
2092
+ * {@link CmcdReporter.createRequestReport} writes there; every other key
2093
+ * is left untouched and stays visible to an event target's `transform`
2094
+ * via its `request` argument.
1901
2095
  *
1902
2096
  * A reporter given a concrete `C` requires the request to satisfy it, so a
1903
2097
  * request the configured transforms could not read is rejected here rather
1904
2098
  * than reaching them; see {@link CmcdReporterCustomData}.
1905
2099
  *
2100
+ * The event is attributed to the session that issued the request, and to
2101
+ * nothing else: the provenance record that
2102
+ * {@link CmcdReporter.createRequestReport} stored on the request's
2103
+ * `customData` (under {@link CMCD_REQUEST_PROVENANCE}) selects the
2104
+ * session by its `sid`, or the response is dropped rather than
2105
+ * relabeled. There is no other key: a record lost to a serialization
2106
+ * boundary (and not restored; see the bridge notes on
2107
+ * {@link CMCD_REQUEST_PROVENANCE}) and a session no longer retained
2108
+ * (see `CmcdReporterConfig.sessionRetention`) both drop, and a per-call
2109
+ * `data.sid` cannot substitute. The record is honored wherever its
2110
+ * `sid` resolves: a request decorated by another reporter configured
2111
+ * with the same session attributes here, as does a hand-built record.
2112
+ * A response that completes after a `sid` change reports under its own
2113
+ * retained session, with that session's data snapshot and sequence
2114
+ * numbers.
2115
+ *
2116
+ * Request-time report keys come from the record's encoded per-call
2117
+ * `data` snapshot, decoded fresh per response, and the record's `cid`
2118
+ * reports in place of the session's current one, so a response that
2119
+ * completes after a mid-session content change keeps the meaning it
2120
+ * had when its request was issued.
2121
+ *
1906
2122
  * @typeParam RD - The `customData` this request carries. Defaults to the
1907
2123
  * reporter's own `C`.
1908
2124
  *
1909
2125
  * @param response - The HTTP response received.
1910
2126
  * @param data - Additional CMCD data to include with the event.
1911
2127
  * Values provided here override any auto-derived values.
2128
+ * Session-owned keys (`sid`, `msd`) supplied here are
2129
+ * ignored; attribution is by the provenance record alone.
1912
2130
  */
1913
2131
  recordResponseReceived(response, data = {}) {
1914
2132
  const { request } = response;
1915
2133
  const url = data.url ?? request?.url;
1916
2134
  if (!url) return;
2135
+ const provenance = request.customData?.[CMCD_REQUEST_PROVENANCE];
2136
+ const session = this.resolveSession(provenance);
2137
+ if (!session) return;
2138
+ const cmcd = decodeSnapshot(provenance);
2139
+ const { cid } = provenance;
1917
2140
  const urlObj = new URL(url);
1918
2141
  urlObj.searchParams.delete(CMCD_PARAM);
1919
2142
  const derived = {
@@ -1928,14 +2151,28 @@ var CmcdReporter = class {
1928
2151
  }
1929
2152
  if (timing.duration != null) derived.ttlb = Math.round(timing.duration);
1930
2153
  }
1931
- const cmcd = request.customData?.cmcd ?? {};
1932
- this.emitEvent(CMCD_EVENT_RESPONSE_RECEIVED, {
2154
+ this.emitEvent(session, CMCD_EVENT_RESPONSE_RECEIVED, {
2155
+ ...typeof cid === "string" && cid ? { cid } : void 0,
1933
2156
  ...cmcd,
1934
2157
  ...derived,
1935
2158
  ...data
1936
2159
  }, request);
1937
2160
  }
1938
2161
  /**
2162
+ * Resolves the session a response belongs to: the provenance record's
2163
+ * `sid` names one of this reporter's retained sessions, or the response
2164
+ * is dropped. There is no other key — a record that was lost, or that
2165
+ * names an evicted or never-seen `sid`, resolves nothing, and
2166
+ * attributing it anywhere else would relabel it. The `sid` is read
2167
+ * structurally, so a JSON-revived copy of a record attributes exactly,
2168
+ * and a hand-built record naming a retained session is honored.
2169
+ */
2170
+ resolveSession(provenance) {
2171
+ if (provenance === null || typeof provenance !== "object") return;
2172
+ const { sid } = provenance;
2173
+ return typeof sid === "string" ? this.sessions.get(sid) : void 0;
2174
+ }
2175
+ /**
1939
2176
  * Applies the CMCD request report data to the request. Called by the player
1940
2177
  * before sending the request.
1941
2178
  *
@@ -1956,6 +2193,34 @@ var CmcdReporter = class {
1956
2193
  return !!this.config.enabledKeys?.length;
1957
2194
  }
1958
2195
  /**
2196
+ * The provenance record to stamp on a returned request: the session's
2197
+ * frozen base record, extended with the caller's per-call data encoded
2198
+ * as a CMCD string when there is any. Requests without per-call data
2199
+ * share the base record, so decoration stays a single property write.
2200
+ * The snapshot never contains reporter-stamped fields, and a value that
2201
+ * cannot encode contributes no snapshot rather than failing the
2202
+ * request; its response still attributes and reports derived keys over
2203
+ * session data, matching `decodeSnapshot()`'s tolerance on the read
2204
+ * side.
2205
+ */
2206
+ createRequestProvenance(data, baseUrl) {
2207
+ const base = this.session.provenance;
2208
+ if (!data) return base;
2209
+ try {
2210
+ const encoded = encodeCmcd(data, {
2211
+ version: this.config.version,
2212
+ reportingMode: CMCD_REQUEST_MODE,
2213
+ baseUrl
2214
+ });
2215
+ return encoded ? Object.freeze({
2216
+ ...base,
2217
+ data: encoded
2218
+ }) : base;
2219
+ } catch {
2220
+ return base;
2221
+ }
2222
+ }
2223
+ /**
1959
2224
  * Creates a new request with the CMCD request report data applied. Called by the player
1960
2225
  * before sending the request.
1961
2226
  *
@@ -1980,13 +2245,16 @@ var CmcdReporter = class {
1980
2245
  headers: { ...headers },
1981
2246
  customData: {
1982
2247
  ...customData,
1983
- cmcd: {}
2248
+ cmcd: {},
2249
+ [CMCD_REQUEST_PROVENANCE]: this.createRequestProvenance(data, request.url)
1984
2250
  }
1985
2251
  };
1986
2252
  if (!this.config.enabledKeys?.length || !report.url) return report;
2253
+ const session = this.session;
1987
2254
  const merged = {
1988
- ...this.data,
1989
- ...data
2255
+ ...session.data,
2256
+ ...data,
2257
+ sid: session.sid
1990
2258
  };
1991
2259
  const { transform } = this.config;
1992
2260
  let cmcdData = merged;
@@ -1995,19 +2263,20 @@ var CmcdReporter = class {
1995
2263
  cmcdData = transform(merged, request);
1996
2264
  if (cmcdData == null) return report;
1997
2265
  }
1998
- cmcdData.sn = this.requestTarget.sn++;
1999
- if (!isNaN(this.msd) && !this.requestTarget.msdSent) {
2000
- cmcdData.msd = this.msd;
2001
- this.requestTarget.msdSent = true;
2002
- }
2266
+ cmcdData.sn = session.requestTarget.sn++;
2267
+ cmcdData.sid = session.sid;
2268
+ const sendMsd = !isNaN(session.msd) && !session.requestTarget.msdSent;
2269
+ if (sendMsd) cmcdData.msd = session.msd;
2270
+ else delete cmcdData.msd;
2003
2271
  const url = new URL(report.url);
2004
2272
  const options = createEncodingOptions(CMCD_REQUEST_MODE, this.config, report.url);
2005
- const cmcd = report.customData.cmcd = prepareCmcdData(cmcdData, options);
2273
+ const cmcd = report.customData.cmcd = copyReportValues(prepareCmcdData(cmcdData, options));
2274
+ if (sendMsd && cmcd.msd !== void 0) session.requestTarget.msdSent = true;
2275
+ const encoded = encodePreparedCmcd(cmcd);
2006
2276
  switch (this.config.transmissionMode) {
2007
2277
  case CMCD_QUERY:
2008
- const param = encodePreparedCmcd(cmcd);
2009
- if (param) {
2010
- url.searchParams.set(CMCD_PARAM, param);
2278
+ if (encoded) {
2279
+ url.searchParams.set(CMCD_PARAM, encoded);
2011
2280
  report.url = url.toString();
2012
2281
  }
2013
2282
  break;
@@ -2024,16 +2293,19 @@ var CmcdReporter = class {
2024
2293
  */
2025
2294
  processEventTargets(flush = false) {
2026
2295
  let reprocess = false;
2027
- this.eventTargets.forEach((target, config) => {
2028
- const { queue } = target;
2029
- if (!queue.length) return;
2030
- if (queue.length < config.batchSize && !flush) return;
2031
- const deleteCount = flush ? queue.length : config.batchSize;
2032
- const events = queue.splice(0, deleteCount);
2033
- this.sendEventReport(config, events).catch(() => {
2034
- target.queue.unshift(...events);
2296
+ this.sessions.forEach((session) => {
2297
+ const drain = flush || session !== this.session;
2298
+ session.eventTargets.forEach((target, config) => {
2299
+ const { queue } = target;
2300
+ if (target.disposed || !queue.length) return;
2301
+ if (queue.length < config.batchSize && !drain) return;
2302
+ const deleteCount = drain ? queue.length : config.batchSize;
2303
+ const events = queue.splice(0, deleteCount);
2304
+ this.sendEventReport(session, config, events).catch(() => {
2305
+ target.queue.unshift(...events);
2306
+ });
2307
+ reprocess ||= queue.length > 0;
2035
2308
  });
2036
- reprocess ||= queue.length > 0;
2037
2309
  });
2038
2310
  if (reprocess) this.processEventTargets();
2039
2311
  }
@@ -2041,44 +2313,42 @@ var CmcdReporter = class {
2041
2313
  * Sends an event report. Called by the reporter when a batch is ready to be sent.
2042
2314
  *
2043
2315
  * @param config - The target config to send the event report to.
2044
- * @param data - The data to send in the event report.
2316
+ * @param data - The encoded report lines to send in the event report.
2045
2317
  */
2046
- async sendEventReport(config, data) {
2047
- const options = createEncodingOptions(CMCD_EVENT_MODE, config);
2318
+ async sendEventReport(session, config, data) {
2048
2319
  const { status } = await this.requester({
2049
2320
  url: config.url,
2050
2321
  method: "POST",
2051
2322
  headers: { "Content-Type": CMCD_MIME_TYPE },
2052
- body: data.map((item) => encodeCmcd(item, options)).join("\n") + "\n"
2323
+ body: data.join("\n") + "\n"
2053
2324
  });
2054
- if (status === 410) this.disposeEventTarget(config);
2325
+ if (status === 410) this.disposeEventTarget(session, config);
2055
2326
  else if (status === 429 || status > 499 && status < 600) throw new Error(`Event report failed with status ${status}`);
2056
2327
  }
2057
2328
  /**
2058
- * Cancels the time-interval timer for an event target and clears the stored id.
2059
- * Safe to call when no timer is armed (clearInterval(undefined) is a no-op).
2060
- */
2061
- disarmInterval(target) {
2062
- clearInterval(target.intervalId);
2063
- target.intervalId = void 0;
2064
- }
2065
- /**
2066
- * Permanently removes an event target: cancels its timer and removes it from the
2067
- * eventTargets map. Used when the collector signals the target is gone (HTTP 410).
2329
+ * Cancels the time-interval timer for an event target config and clears the
2330
+ * stored id. Safe to call when no timer is armed (clearInterval(undefined)
2331
+ * is a no-op).
2068
2332
  */
2069
- disposeEventTarget(config) {
2070
- const target = this.eventTargets.get(config);
2071
- if (!target) return;
2072
- this.disarmInterval(target);
2073
- this.eventTargets.delete(config);
2333
+ disarmInterval(config) {
2334
+ clearInterval(this.intervals.get(config));
2335
+ this.intervals.delete(config);
2074
2336
  }
2075
2337
  /**
2076
- * Resets the session related data. Called when the session ID changes.
2338
+ * Silences an event target URL for the remainder of its session: drops the
2339
+ * queues and blocks further enqueues for every config in the session that
2340
+ * reports to the URL, per CTA-5004-B's per-target-URL suppression scope,
2341
+ * and cancels their timers when the session is the live one. Used when the
2342
+ * collector signals the target is gone (HTTP 410). A session started after
2343
+ * the disposal is unaffected, since it gets fresh target state.
2077
2344
  */
2078
- resetSession() {
2079
- this.eventTargets.forEach((target) => target.sn = 0);
2080
- this.requestTarget.sn = 0;
2081
- this.lastEmitted = {};
2345
+ disposeEventTarget(session, config) {
2346
+ session.eventTargets.forEach((target, sibling) => {
2347
+ if (sibling.url !== config.url || target.disposed) return;
2348
+ target.disposed = true;
2349
+ target.queue.length = 0;
2350
+ if (session === this.session) this.disarmInterval(sibling);
2351
+ });
2082
2352
  }
2083
2353
  };
2084
2354
 
@@ -2141,51 +2411,6 @@ const CmcdValidationSeverity = {
2141
2411
  WARNING: CMCD_VALIDATION_SEVERITY_WARNING
2142
2412
  };
2143
2413
 
2144
- //#endregion
2145
- //#region src/upConvertToV2.ts
2146
- /**
2147
- * Up-convert V1 CMCD data to V2 format.
2148
- *
2149
- * - Wraps plain scalar values in arrays for inner-list keys.
2150
- * - Wraps `nor` string in an array.
2151
- *
2152
- * If the data is already V2 (has `v: 2`), it is returned unchanged.
2153
- *
2154
- * @internal
2155
- */
2156
- function upConvertToV2(obj) {
2157
- if (obj["v"] === CMCD_V2) return obj;
2158
- const result = {};
2159
- for (const [key, value] of Object.entries(obj)) {
2160
- if (value == null) {
2161
- result[key] = value;
2162
- continue;
2163
- }
2164
- if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(value)) result[key] = [value];
2165
- else if (key === "nor" && typeof value === "string") result[key] = [value];
2166
- else result[key] = value;
2167
- }
2168
- return result;
2169
- }
2170
-
2171
- //#endregion
2172
- //#region src/decodeCmcd.ts
2173
- function reduceValue(value) {
2174
- if (Array.isArray(value)) return value.map(reduceValue);
2175
- if (typeof value === "symbol") return symbolToStr(value);
2176
- if (value instanceof SfItem && !value.params) return reduceValue(value.value);
2177
- if (typeof value === "string") return value;
2178
- return value;
2179
- }
2180
- function decodeCmcd(cmcd, options) {
2181
- if (!cmcd) return {};
2182
- const sfDict = decodeSfDict(cmcd);
2183
- const result = {};
2184
- for (const [key, item] of Object.entries(sfDict)) result[key] = reduceValue(item.value);
2185
- if (options?.convertToLatest) return upConvertToV2(result);
2186
- return result;
2187
- }
2188
-
2189
2414
  //#endregion
2190
2415
  //#region src/ensureHeaders.ts
2191
2416
  /**
@@ -3106,5 +3331,5 @@ function extractHeaderRecord(headers) {
3106
3331
  }
3107
3332
 
3108
3333
  //#endregion
3109
- export { CMCD_DEFAULT_TIME_INTERVAL, CMCD_EVENT_AD_BREAK_END, CMCD_EVENT_AD_BREAK_START, CMCD_EVENT_AD_END, CMCD_EVENT_AD_START, CMCD_EVENT_BACKGROUNDED_MODE, CMCD_EVENT_BITRATE_CHANGE, CMCD_EVENT_CONTENT_ID, CMCD_EVENT_CUSTOM_EVENT, CMCD_EVENT_ERROR, CMCD_EVENT_KEYS, CMCD_EVENT_MODE, CMCD_EVENT_MUTE, CMCD_EVENT_PLAYBACK_RATE, CMCD_EVENT_PLAYER_COLLAPSE, CMCD_EVENT_PLAYER_EXPAND, CMCD_EVENT_PLAY_STATE, CMCD_EVENT_RESPONSE_RECEIVED, CMCD_EVENT_SKIP, CMCD_EVENT_TIME_INTERVAL, CMCD_EVENT_UNMUTE, CMCD_FORMATTER_MAP, CMCD_HEADERS, CMCD_HEADER_FIELDS, CMCD_HEADER_MAP, CMCD_JSON, CMCD_KEYS, CMCD_MIME_TYPE, CMCD_OBJECT, CMCD_PARAM, CMCD_QUERY, CMCD_RECORDED_REPORT_MODE_EVENT, CMCD_RECORDED_REPORT_MODE_HEADER, CMCD_RECORDED_REPORT_MODE_QUERY, CMCD_RECORDED_REQUEST_TYPE_EVENT, CMCD_RECORDED_REQUEST_TYPE_MANIFEST, CMCD_RECORDED_REQUEST_TYPE_SEGMENT, CMCD_RECORDED_REQUEST_TYPE_UNKNOWN, CMCD_REQUEST, CMCD_REQUEST_KEYS, CMCD_REQUEST_MODE, CMCD_RESPONSE_KEYS, CMCD_SESSION, CMCD_STATUS, CMCD_V1, CMCD_V1_KEYS, CMCD_V2, CMCD_VALIDATION_SEVERITY_ERROR, CMCD_VALIDATION_SEVERITY_WARNING, CmcdEventType, CmcdHeaderField, CmcdObjectType, CmcdPlayerState, CmcdRecordedReportMode, CmcdRecordedRequestType, CmcdReportRecorder, CmcdReporter, CmcdReportingMode, CmcdStreamType, CmcdStreamingFormat, CmcdTransmissionMode, CmcdValidationSeverity, appendCmcdHeaders, appendCmcdQuery, createFetchTransport, createXhrTransport, decodeCmcd, encodeCmcd, fromCmcdHeaders, fromCmcdQuery, fromCmcdUrl, groupCmcdHeaders, isCmcdCustomKey, isCmcdEventKey, isCmcdRequestKey, isCmcdResponseReceivedKey, isCmcdV1Data, isCmcdV1Key, isCmcdV2Data, prepareCmcdData, toCmcdHeaders, toCmcdQuery, toCmcdUrl, toCmcdValue, validateCmcd, validateCmcdEventReport, validateCmcdEvents, validateCmcdHeaders, validateCmcdKeys, validateCmcdRequest, validateCmcdStructure, validateCmcdValues };
3334
+ export { CMCD_DEFAULT_TIME_INTERVAL, CMCD_EVENT_AD_BREAK_END, CMCD_EVENT_AD_BREAK_START, CMCD_EVENT_AD_END, CMCD_EVENT_AD_START, CMCD_EVENT_BACKGROUNDED_MODE, CMCD_EVENT_BITRATE_CHANGE, CMCD_EVENT_CONTENT_ID, CMCD_EVENT_CUSTOM_EVENT, CMCD_EVENT_ERROR, CMCD_EVENT_KEYS, CMCD_EVENT_MODE, CMCD_EVENT_MUTE, CMCD_EVENT_PLAYBACK_RATE, CMCD_EVENT_PLAYER_COLLAPSE, CMCD_EVENT_PLAYER_EXPAND, CMCD_EVENT_PLAY_STATE, CMCD_EVENT_RESPONSE_RECEIVED, CMCD_EVENT_SKIP, CMCD_EVENT_TIME_INTERVAL, CMCD_EVENT_UNMUTE, CMCD_FORMATTER_MAP, CMCD_HEADERS, CMCD_HEADER_FIELDS, CMCD_HEADER_MAP, CMCD_JSON, CMCD_KEYS, CMCD_MIME_TYPE, CMCD_OBJECT, CMCD_PARAM, CMCD_QUERY, CMCD_RECORDED_REPORT_MODE_EVENT, CMCD_RECORDED_REPORT_MODE_HEADER, CMCD_RECORDED_REPORT_MODE_QUERY, CMCD_RECORDED_REQUEST_TYPE_EVENT, CMCD_RECORDED_REQUEST_TYPE_MANIFEST, CMCD_RECORDED_REQUEST_TYPE_SEGMENT, CMCD_RECORDED_REQUEST_TYPE_UNKNOWN, CMCD_REQUEST, CMCD_REQUEST_KEYS, CMCD_REQUEST_MODE, CMCD_REQUEST_PROVENANCE, CMCD_RESPONSE_KEYS, CMCD_SESSION, CMCD_STATUS, CMCD_V1, CMCD_V1_KEYS, CMCD_V2, CMCD_VALIDATION_SEVERITY_ERROR, CMCD_VALIDATION_SEVERITY_WARNING, CmcdEventType, CmcdHeaderField, CmcdObjectType, CmcdPlayerState, CmcdRecordedReportMode, CmcdRecordedRequestType, CmcdReportRecorder, CmcdReporter, CmcdReportingMode, CmcdStreamType, CmcdStreamingFormat, CmcdTransmissionMode, CmcdValidationSeverity, appendCmcdHeaders, appendCmcdQuery, createFetchTransport, createXhrTransport, decodeCmcd, encodeCmcd, fromCmcdHeaders, fromCmcdQuery, fromCmcdUrl, groupCmcdHeaders, isCmcdCustomKey, isCmcdEventKey, isCmcdRequestKey, isCmcdResponseReceivedKey, isCmcdV1Data, isCmcdV1Key, isCmcdV2Data, prepareCmcdData, toCmcdHeaders, toCmcdQuery, toCmcdUrl, toCmcdValue, validateCmcd, validateCmcdEventReport, validateCmcdEvents, validateCmcdHeaders, validateCmcdKeys, validateCmcdRequest, validateCmcdStructure, validateCmcdValues };
3110
3335
  //# sourceMappingURL=index.js.map