@aeroswarm/client 0.1.12 → 0.1.14

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
@@ -140,12 +140,13 @@ var AeroWsClient = class {
140
140
  this._requestSnapshot();
141
141
  }
142
142
  this.lastSeq = msg.seq;
143
+ const meta = { serverTime: msg.serverTime, seq: msg.seq };
143
144
  switch (msg.type) {
144
145
  case "snapshot":
145
- this.callbacks.onSnapshot?.(msg.payload.tracks);
146
+ this.callbacks.onSnapshot?.(msg.payload.tracks, meta);
146
147
  break;
147
148
  case "track.updated":
148
- this.callbacks.onTrackUpdate?.(msg.payload.track);
149
+ this.callbacks.onTrackUpdate?.(msg.payload.track, meta);
149
150
  break;
150
151
  case "track.removed":
151
152
  this.callbacks.onTrackRemoved?.(msg.payload);
@@ -313,6 +314,85 @@ function createAeroTrpcClient(endpoint, token) {
313
314
  });
314
315
  }
315
316
 
317
+ // src/sdk/api/response.ts
318
+ var ENVELOPE_HEADER = "x-aero-envelope";
319
+ var STATUS_CODES = [
320
+ [401, "UNAUTHORIZED"],
321
+ [403, "FORBIDDEN"],
322
+ [404, "NOT_FOUND"],
323
+ [400, "BAD_REQUEST"],
324
+ [409, "CONFLICT"],
325
+ [429, "RATE_LIMITED"],
326
+ [503, "UNAVAILABLE"],
327
+ [500, "INTERNAL"]
328
+ ];
329
+ function codeForStatus(status) {
330
+ const hit = STATUS_CODES.find(([s]) => s === status);
331
+ if (hit) return hit[1];
332
+ return status >= 500 ? "INTERNAL" : "BAD_REQUEST";
333
+ }
334
+ var AeroApiError = class extends Error {
335
+ status;
336
+ code;
337
+ /** Retrying later may succeed (rate limit, server busy or failing). */
338
+ recoverable;
339
+ details;
340
+ constructor(init) {
341
+ super(init.message);
342
+ this.name = "AeroApiError";
343
+ this.status = init.status;
344
+ this.code = init.code;
345
+ this.recoverable = init.recoverable;
346
+ this.details = init.details;
347
+ }
348
+ };
349
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
350
+ function isEnvelope(body) {
351
+ return isObject(body) && body.ok === true && "data" in body;
352
+ }
353
+ function isFailure(body) {
354
+ return isObject(body) && body.ok === false;
355
+ }
356
+ function errorFrom(res, body, text, label) {
357
+ const prefix = label ? `${label} \u2192 ${res.status}` : `HTTP ${res.status}`;
358
+ const failure = isFailure(body) ? body : void 0;
359
+ const e = failure?.error;
360
+ if (isObject(e)) {
361
+ const code = typeof e.code === "string" ? e.code : codeForStatus(res.status);
362
+ const message2 = typeof e.message === "string" ? e.message : res.statusText || prefix;
363
+ return new AeroApiError({
364
+ status: res.status,
365
+ code,
366
+ message: `${prefix}: ${message2}`,
367
+ recoverable: typeof e.recoverable === "boolean" ? e.recoverable : res.status >= 500,
368
+ details: e.details
369
+ });
370
+ }
371
+ const message = typeof e === "string" ? e : text.slice(0, 200) || res.statusText;
372
+ return new AeroApiError({
373
+ status: res.status,
374
+ code: codeForStatus(res.status),
375
+ message: message ? `${prefix}: ${message}` : prefix,
376
+ recoverable: res.status >= 500 || res.status === 429,
377
+ details: failure?.detail
378
+ });
379
+ }
380
+ async function readApiBody(res, label) {
381
+ const text = await res.text();
382
+ let body;
383
+ try {
384
+ body = text ? JSON.parse(text) : void 0;
385
+ } catch {
386
+ body = void 0;
387
+ }
388
+ if (!res.ok || isFailure(body)) throw errorFrom(res, body, text, label);
389
+ return body;
390
+ }
391
+ async function parseApiResponse(res, label) {
392
+ const body = await readApiBody(res, label);
393
+ return isEnvelope(body) ? body.data : body;
394
+ }
395
+
316
396
  // src/sdk/api/rest.ts
317
397
  var AeroRestClient = class {
318
398
  constructor(endpoint, token) {
@@ -330,8 +410,7 @@ var AeroRestClient = class {
330
410
  },
331
411
  body: body ? JSON.stringify(body) : void 0
332
412
  });
333
- if (!res.ok) throw new Error(`${method} ${path} \u2192 ${res.status}`);
334
- return res.json();
413
+ return await readApiBody(res, `${method} ${path}`);
335
414
  }
336
415
  createSource(body) {
337
416
  return this.req("POST", "/v1/sources", body);
@@ -553,9 +632,10 @@ var AeroSwarm = class _AeroSwarm {
553
632
  async federatedFetch(base, path, query) {
554
633
  const url = new URL(`${base}${path}`);
555
634
  for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
556
- const res = await fetch(url, { headers: { Authorization: `Bearer ${this.opts.token}` } });
557
- if (!res.ok) throw new Error(`Federated request failed: ${res.status} ${res.statusText} (${url.pathname})`);
558
- return res.json();
635
+ const res = await fetch(url, {
636
+ headers: { Authorization: `Bearer ${this.opts.token}`, [ENVELOPE_HEADER]: "1" }
637
+ });
638
+ return parseApiResponse(res, `Federated request ${url.pathname}`);
559
639
  }
560
640
  tracks = {
561
641
  subscribe: (opts, callbacks) => {
@@ -567,15 +647,15 @@ var AeroSwarm = class _AeroSwarm {
567
647
  this._status = s;
568
648
  callbacks.onStatusChange?.(s);
569
649
  },
570
- onSnapshot: (tracks) => {
650
+ onSnapshot: (tracks, meta) => {
571
651
  const labeled = this.cache.applySnapshot(tracks);
572
652
  this._lastUpdateAt = Date.now();
573
- callbacks.onSnapshot?.(labeled);
653
+ callbacks.onSnapshot?.(labeled, meta);
574
654
  },
575
- onTrackUpdate: (track) => {
655
+ onTrackUpdate: (track, meta) => {
576
656
  const labeled = this.cache.applyUpdate(track);
577
657
  this._lastUpdateAt = Date.now();
578
- callbacks.onTrackUpdate?.(labeled);
658
+ callbacks.onTrackUpdate?.(labeled, meta);
579
659
  },
580
660
  onTrackRemoved: (ev) => {
581
661
  this.cache.applyRemoved(ev);
@@ -600,7 +680,7 @@ var AeroSwarm = class _AeroSwarm {
600
680
  reconnect: () => this.wsClient.reconnect(),
601
681
  stream: (opts) => {
602
682
  const queue = [];
603
- let resolve = null;
683
+ let resolve2 = null;
604
684
  let done = false;
605
685
  this.wsClient.connect({ encoding: "json", ...opts }, {
606
686
  onSnapshot: (tracks) => {
@@ -608,9 +688,9 @@ var AeroSwarm = class _AeroSwarm {
608
688
  },
609
689
  onTrackUpdate: (raw) => {
610
690
  const t = this.cache.applyUpdate(raw);
611
- if (resolve) {
612
- resolve({ value: t, done: false });
613
- resolve = null;
691
+ if (resolve2) {
692
+ resolve2({ value: t, done: false });
693
+ resolve2 = null;
614
694
  } else queue.push(t);
615
695
  },
616
696
  onStatusChange: (s) => {
@@ -623,7 +703,7 @@ var AeroSwarm = class _AeroSwarm {
623
703
  if (queue.length) return Promise.resolve({ value: queue.shift(), done: false });
624
704
  if (done) return Promise.resolve({ value: void 0, done: true });
625
705
  return new Promise((r) => {
626
- resolve = r;
706
+ resolve2 = r;
627
707
  });
628
708
  },
629
709
  return: async () => {
@@ -786,17 +866,357 @@ var AeroSwarm = class _AeroSwarm {
786
866
 
787
867
  // src/shared/utils/index.ts
788
868
  import { isRenderableTrack as isRenderableTrack2, haversineKm, isInsideBbox, toCurrentTrack, clamp } from "@aeroswarm/core/utils";
869
+
870
+ // src/shared/motion/index.ts
871
+ var KT_TO_MS = 0.514444;
872
+ var M_PER_DEG = 111320;
873
+ var MAX_PLAUSIBLE_KT = 1500;
874
+ var OFFSET_WINDOW = 20;
875
+ var ALT_EPSILON_FT = 10;
876
+ var HEADING_EPSILON_DEG = 0.5;
877
+ var SURFACE_ALT_FT = 150;
878
+ var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
879
+ var smoothstep = (x) => {
880
+ const c = clamp01(x);
881
+ return c * c * (3 - 2 * c);
882
+ };
883
+ var wrapDeg = (d) => ((d + 180) % 360 + 360) % 360 - 180;
884
+ var norm360 = (d) => (d % 360 + 360) % 360;
885
+ var cosLat = (lat) => Math.max(0.01, Math.cos(lat * Math.PI / 180));
886
+ function validHeading(h) {
887
+ return h != null && Number.isFinite(h) && h >= 0 && h <= 360 ? h % 360 : null;
888
+ }
889
+ function move(lat, lon, bearingDeg, distM) {
890
+ const rad = bearingDeg * Math.PI / 180;
891
+ const dLat = distM * Math.cos(rad) / M_PER_DEG;
892
+ const dLon = distM * Math.sin(rad) / (M_PER_DEG * cosLat(lat + dLat / 2));
893
+ return { lat: lat + dLat, lon: lon + dLon };
894
+ }
895
+ function distanceM(aLat, aLon, bLat, bLon) {
896
+ const dy = (bLat - aLat) * M_PER_DEG;
897
+ const dx = (bLon - aLon) * M_PER_DEG * cosLat((aLat + bLat) / 2);
898
+ return Math.hypot(dx, dy);
899
+ }
900
+ var headingChanged = (a, b) => a === null || b === null ? a !== b : Math.abs(wrapDeg(a - b)) > HEADING_EPSILON_DEG;
901
+ var altitudeChanged = (a, b) => a === null || b === null ? a !== b : Math.abs(a - b) >= ALT_EPSILON_FT;
902
+ var defaultIsSurface = (t) => t.targetType === "vehicle" || t.altitudeFt != null && t.altitudeFt <= SURFACE_ALT_FT;
903
+ function resolve(o) {
904
+ return {
905
+ blendMs: o.blendMs ?? 1500,
906
+ headingSmoothMs: o.headingSmoothMs ?? 800,
907
+ maxExtrapolateMs: o.maxExtrapolateMs ?? { air: 2e4, surface: 8e3 },
908
+ minMovingKt: o.minMovingKt ?? 5,
909
+ snapDistanceM: o.snapDistanceM ?? 5e3,
910
+ frameHz: o.frameHz ?? 30,
911
+ epsilonM: o.epsilonM ?? 0.5,
912
+ extrapolate: o.extrapolate ?? true,
913
+ isSurface: o.isSurface ?? defaultIsSurface,
914
+ clock: o.clock ?? Date.now,
915
+ scheduler: o.scheduler
916
+ };
917
+ }
918
+ function defaultScheduler() {
919
+ const g = globalThis;
920
+ if (typeof g.requestAnimationFrame === "function" && typeof g.cancelAnimationFrame === "function") {
921
+ return {
922
+ request: (cb) => g.requestAnimationFrame(cb),
923
+ cancel: (h) => g.cancelAnimationFrame(h)
924
+ };
925
+ }
926
+ return {
927
+ request: (cb) => setTimeout(() => cb(Date.now()), 16),
928
+ cancel: (h) => clearTimeout(h)
929
+ };
930
+ }
931
+ var Motion = class {
932
+ o;
933
+ entries = /* @__PURE__ */ new Map();
934
+ /** Tracks whose picture can still change. Everything else costs nothing per frame. */
935
+ active = /* @__PURE__ */ new Set();
936
+ listeners = /* @__PURE__ */ new Set();
937
+ offsets = [];
938
+ offset = 0;
939
+ handle = null;
940
+ lastFrameAt = Number.NEGATIVE_INFINITY;
941
+ constructor(options) {
942
+ this.o = resolve(options);
943
+ }
944
+ get size() {
945
+ return this.entries.size;
946
+ }
947
+ offsetMs() {
948
+ return this.offset;
949
+ }
950
+ // ── time ────────────────────────────────────────────────────────────────
951
+ /** Browser clock + learned offset = the server's clock. */
952
+ serverNow(at) {
953
+ return (at ?? this.o.clock()) + this.offset;
954
+ }
955
+ /**
956
+ * `serverTime - browserNow` is the true offset MINUS the network delay, so the
957
+ * largest recent value is the one with the least delay, i.e. the best estimate.
958
+ */
959
+ observe(serverTime) {
960
+ if (!Number.isFinite(serverTime)) return;
961
+ this.offsets.push(serverTime - this.o.clock());
962
+ if (this.offsets.length > OFFSET_WINDOW) this.offsets.shift();
963
+ this.offset = Math.max(...this.offsets);
964
+ }
965
+ cap(e) {
966
+ const m = this.o.maxExtrapolateMs;
967
+ return e.surface ? m.surface : m.air;
968
+ }
969
+ // ── model ───────────────────────────────────────────────────────────────
970
+ posAt(e, s) {
971
+ const cap = this.cap(e);
972
+ let elapsed = Math.min(Math.max(0, s - e.fixAt), cap);
973
+ if (e.frozen !== null) elapsed = Math.min(elapsed, e.frozen);
974
+ if (!this.o.extrapolate || e.course === null || e.speedMs <= 0) {
975
+ return { lat: e.fixLat, lon: e.fixLon, extrapolating: false };
976
+ }
977
+ const p = move(e.fixLat, e.fixLon, e.course, e.speedMs * elapsed / 1e3);
978
+ return { ...p, extrapolating: e.frozen === null && elapsed < cap };
979
+ }
980
+ sampleAt(e, s) {
981
+ const p = this.posAt(e, s);
982
+ const rest = 1 - smoothstep((s - e.blendStart) / this.o.blendMs);
983
+ let headingDeg = null;
984
+ if (e.hdgTo !== null) {
985
+ const from = e.hdgFrom ?? e.hdgTo;
986
+ const left = 1 - smoothstep((s - e.hdgStart) / this.o.headingSmoothMs);
987
+ headingDeg = norm360(e.hdgTo + wrapDeg(from - e.hdgTo) * left);
988
+ }
989
+ let altitudeFt = null;
990
+ if (e.altTo !== null) {
991
+ const k = smoothstep((s - e.altStart) / this.o.blendMs);
992
+ altitudeFt = e.altFrom === null ? e.altTo : e.altFrom + (e.altTo - e.altFrom) * k;
993
+ }
994
+ return {
995
+ lat: p.lat + e.errLat * rest,
996
+ lon: p.lon + e.errLon * rest,
997
+ headingDeg,
998
+ altitudeFt,
999
+ moving: e.speedMs > 0,
1000
+ extrapolating: p.extrapolating
1001
+ };
1002
+ }
1003
+ // ── input ───────────────────────────────────────────────────────────────
1004
+ update(t, meta) {
1005
+ if (meta?.serverTime !== void 0) this.observe(meta.serverTime);
1006
+ if (t.lat == null || t.lon == null || !Number.isFinite(t.lat) || !Number.isFinite(t.lon)) {
1007
+ this.remove(t.trackId);
1008
+ return;
1009
+ }
1010
+ const s = this.serverNow();
1011
+ const e = this.entries.get(t.trackId);
1012
+ if (!e) {
1013
+ this.create(t, s);
1014
+ return;
1015
+ }
1016
+ this.refresh(e, t, s);
1017
+ }
1018
+ moving(t, course) {
1019
+ const kt = t.groundSpeedKt;
1020
+ return course !== null && kt != null && Number.isFinite(kt) && kt >= this.o.minMovingKt && kt <= MAX_PLAUSIBLE_KT;
1021
+ }
1022
+ create(t, s) {
1023
+ const course = validHeading(t.headingDeg);
1024
+ const moving = this.moving(t, course);
1025
+ const e = {
1026
+ id: t.trackId,
1027
+ fixLat: t.lat,
1028
+ fixLon: t.lon,
1029
+ fixAt: t.lastSeenAt,
1030
+ speedMs: moving ? t.groundSpeedKt * KT_TO_MS : 0,
1031
+ course,
1032
+ surface: this.o.isSurface(t),
1033
+ frozen: null,
1034
+ errLat: 0,
1035
+ errLon: 0,
1036
+ blendStart: s,
1037
+ hdgFrom: course,
1038
+ hdgTo: course,
1039
+ hdgStart: s,
1040
+ altFrom: null,
1041
+ altTo: t.altitudeFt,
1042
+ altStart: s,
1043
+ settleAt: 0,
1044
+ emitted: null
1045
+ };
1046
+ if (t.status === "lost") e.frozen = this.frozenAt(e, s);
1047
+ this.entries.set(e.id, e);
1048
+ this.settle(e);
1049
+ this.active.add(e.id);
1050
+ this.ensureLoop();
1051
+ }
1052
+ frozenAt(e, s) {
1053
+ return Math.min(Math.max(0, s - e.fixAt), this.cap(e));
1054
+ }
1055
+ refresh(e, t, s) {
1056
+ e.surface = this.o.isSurface(t);
1057
+ if (t.status === "lost") e.frozen ??= this.frozenAt(e, s);
1058
+ else e.frozen = null;
1059
+ const lat = t.lat;
1060
+ const lon = t.lon;
1061
+ const older = t.lastSeenAt < e.fixAt;
1062
+ const same = t.lastSeenAt === e.fixAt && lat === e.fixLat && lon === e.fixLon;
1063
+ if (older || same) {
1064
+ this.settle(e);
1065
+ this.touch(e);
1066
+ return;
1067
+ }
1068
+ const shown = this.sampleAt(e, s);
1069
+ const course = validHeading(t.headingDeg) ?? e.course;
1070
+ const moving = this.moving(t, course);
1071
+ e.fixLat = lat;
1072
+ e.fixLon = lon;
1073
+ e.fixAt = t.lastSeenAt;
1074
+ e.speedMs = moving ? t.groundSpeedKt * KT_TO_MS : 0;
1075
+ e.course = course;
1076
+ const predicted = this.posAt(e, s);
1077
+ const jump = distanceM(shown.lat, shown.lon, predicted.lat, predicted.lon);
1078
+ const snap = jump > this.o.snapDistanceM;
1079
+ e.errLat = snap ? 0 : shown.lat - predicted.lat;
1080
+ e.errLon = snap ? 0 : shown.lon - predicted.lon;
1081
+ e.blendStart = s;
1082
+ e.hdgFrom = snap ? course : shown.headingDeg ?? course;
1083
+ e.hdgTo = course;
1084
+ e.hdgStart = s;
1085
+ e.altFrom = snap ? t.altitudeFt : shown.altitudeFt;
1086
+ e.altTo = t.altitudeFt;
1087
+ e.altStart = s;
1088
+ this.settle(e);
1089
+ this.touch(e);
1090
+ }
1091
+ /** The server time after which nothing about this track changes on its own. */
1092
+ settle(e) {
1093
+ const ends = [e.blendStart + this.o.blendMs, e.hdgStart + this.o.headingSmoothMs, e.altStart + this.o.blendMs];
1094
+ if (this.o.extrapolate && e.speedMs > 0 && e.frozen === null) ends.push(e.fixAt + this.cap(e));
1095
+ e.settleAt = Math.max(...ends);
1096
+ }
1097
+ touch(e) {
1098
+ this.active.add(e.id);
1099
+ this.ensureLoop();
1100
+ }
1101
+ remove(trackId) {
1102
+ this.entries.delete(trackId);
1103
+ this.active.delete(trackId);
1104
+ }
1105
+ clear() {
1106
+ this.entries.clear();
1107
+ this.active.clear();
1108
+ }
1109
+ // ── output ──────────────────────────────────────────────────────────────
1110
+ sample(trackId, at) {
1111
+ const e = this.entries.get(trackId);
1112
+ return e ? this.sampleAt(e, this.serverNow(at)) : void 0;
1113
+ }
1114
+ differs(a, b) {
1115
+ if (a === null || a.moving !== b.moving || a.extrapolating !== b.extrapolating) return true;
1116
+ if (distanceM(a.lat, a.lon, b.lat, b.lon) > this.o.epsilonM) return true;
1117
+ return headingChanged(a.headingDeg, b.headingDeg) || altitudeChanged(a.altitudeFt, b.altitudeFt);
1118
+ }
1119
+ tick(at) {
1120
+ const s = this.serverNow(at);
1121
+ const changed = /* @__PURE__ */ new Map();
1122
+ for (const id of [...this.active]) {
1123
+ const e = this.entries.get(id);
1124
+ if (!e) {
1125
+ this.active.delete(id);
1126
+ continue;
1127
+ }
1128
+ const sample = this.sampleAt(e, s);
1129
+ if (this.differs(e.emitted, sample)) {
1130
+ e.emitted = sample;
1131
+ changed.set(id, sample);
1132
+ }
1133
+ if (s >= e.settleAt) this.active.delete(id);
1134
+ }
1135
+ return changed;
1136
+ }
1137
+ // ── frame loop ──────────────────────────────────────────────────────────
1138
+ start(onFrame) {
1139
+ this.listeners.add(onFrame);
1140
+ this.ensureLoop();
1141
+ return () => {
1142
+ this.listeners.delete(onFrame);
1143
+ if (this.listeners.size === 0) this.cancelLoop();
1144
+ };
1145
+ }
1146
+ ensureLoop() {
1147
+ if (this.handle !== null || this.listeners.size === 0 || this.active.size === 0) return;
1148
+ const scheduler = this.o.scheduler ?? (this.o.scheduler = defaultScheduler());
1149
+ this.handle = scheduler.request((ts) => this.frame(ts));
1150
+ }
1151
+ cancelLoop() {
1152
+ if (this.handle === null) return;
1153
+ (this.o.scheduler ?? defaultScheduler()).cancel(this.handle);
1154
+ this.handle = null;
1155
+ }
1156
+ frame(ts) {
1157
+ this.handle = null;
1158
+ if (this.listeners.size === 0) return;
1159
+ if (ts - this.lastFrameAt >= 1e3 / this.o.frameHz - 1) {
1160
+ this.lastFrameAt = ts;
1161
+ const changed = this.tick();
1162
+ if (changed.size > 0) {
1163
+ const frame = { at: this.serverNow(), changed };
1164
+ for (const fn of [...this.listeners]) fn(frame);
1165
+ }
1166
+ }
1167
+ this.ensureLoop();
1168
+ }
1169
+ };
1170
+ function createTrackMotion(options = {}) {
1171
+ return new Motion(options);
1172
+ }
1173
+
1174
+ // src/shared/source/index.ts
1175
+ function sourceLabel(source) {
1176
+ switch (source) {
1177
+ case "ADSB":
1178
+ case "ASTERIX_CAT021":
1179
+ return "ADSB";
1180
+ case "ASTERIX_CAT010":
1181
+ case "ASTERIX_CAT011":
1182
+ return "SURFACE";
1183
+ case "ASTERIX_CAT020":
1184
+ return "MLAT";
1185
+ case "ASTERIX_CAT048":
1186
+ return "RADAR";
1187
+ case "ASTERIX_CAT062":
1188
+ return "SYSTEM";
1189
+ case "FUSED":
1190
+ return "FUSED";
1191
+ default: {
1192
+ const unreachable = source;
1193
+ return unreachable;
1194
+ }
1195
+ }
1196
+ }
1197
+ function formatDataSource(track) {
1198
+ const kind = sourceLabel(track.source);
1199
+ const ds = track.dataSource;
1200
+ return ds ? `${kind} ${ds.sac}/${ds.sic}` : kind;
1201
+ }
789
1202
  export {
1203
+ AeroApiError,
790
1204
  AeroReplayClient,
791
1205
  AeroSwarm,
792
1206
  AeroWsClient,
793
1207
  LABEL_COLORS,
794
1208
  TrackCache,
795
1209
  clamp,
1210
+ codeForStatus,
1211
+ createTrackMotion,
1212
+ formatDataSource,
796
1213
  getLabelCategory,
797
1214
  haversineKm,
798
1215
  isInsideBbox,
799
1216
  isRenderableTrack2 as isRenderableTrack,
1217
+ parseApiResponse,
1218
+ readApiBody,
1219
+ sourceLabel,
800
1220
  toCurrentTrack,
801
1221
  withLabel
802
1222
  };