@memberjunction/realtime-runtime 6.2.0-edge.4 → 6.2.0-edge.6

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.
Files changed (31) hide show
  1. package/dist/hosts/IRealtimeMediaHost.d.ts +25 -30
  2. package/dist/hosts/IRealtimeMediaHost.d.ts.map +1 -1
  3. package/dist/index.d.ts +6 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +5 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/session/RealtimeSessionRuntime.d.ts +372 -73
  8. package/dist/session/RealtimeSessionRuntime.d.ts.map +1 -1
  9. package/dist/session/RealtimeSessionRuntime.js +919 -212
  10. package/dist/session/RealtimeSessionRuntime.js.map +1 -1
  11. package/dist/session/RecordingShardDrain.d.ts +59 -0
  12. package/dist/session/RecordingShardDrain.d.ts.map +1 -0
  13. package/dist/session/RecordingShardDrain.js +135 -0
  14. package/dist/session/RecordingShardDrain.js.map +1 -0
  15. package/dist/session/RecordingShardQueue.d.ts +64 -38
  16. package/dist/session/RecordingShardQueue.d.ts.map +1 -1
  17. package/dist/session/RecordingShardQueue.js +106 -84
  18. package/dist/session/RecordingShardQueue.js.map +1 -1
  19. package/dist/session/RecordingShardStore.d.ts +57 -0
  20. package/dist/session/RecordingShardStore.d.ts.map +1 -0
  21. package/dist/session/RecordingShardStore.js +184 -0
  22. package/dist/session/RecordingShardStore.js.map +1 -0
  23. package/dist/session/RecordingShardWindow.d.ts +42 -0
  24. package/dist/session/RecordingShardWindow.d.ts.map +1 -0
  25. package/dist/session/RecordingShardWindow.js +88 -0
  26. package/dist/session/RecordingShardWindow.js.map +1 -0
  27. package/dist/session/RecordingTransport.d.ts +67 -0
  28. package/dist/session/RecordingTransport.d.ts.map +1 -0
  29. package/dist/session/RecordingTransport.js +61 -0
  30. package/dist/session/RecordingTransport.js.map +1 -0
  31. package/package.json +10 -9
@@ -0,0 +1,135 @@
1
+ import { CreateRecordingShardStore } from './RecordingShardStore.js';
2
+ import { FetchRecordingTransport, RecordingBaseURLFromGraphQLURL } from './RecordingTransport.js';
3
+ /** Most stored shards one drain pass re-uploads; the rest wait for the next pass or load. */
4
+ export const MAX_DRAINED_SHARDS_PER_LOAD = 50;
5
+ function describeFailure(failure) {
6
+ return failure instanceof Error ? failure.message : String(failure);
7
+ }
8
+ /**
9
+ * The default recording transport for a provider: MJAPI's REST recording routes, derived from the
10
+ * provider's GraphQL URL, with credentials read per request so a refreshed token is used. Null when
11
+ * the provider has no URL (a test double, a provider that is not GraphQL, or one not configured yet).
12
+ */
13
+ export function CreateRecordingTransportForProvider(provider) {
14
+ // Typed as GraphQL, but read defensively: a test double or a non-GraphQL provider has no ConfigData.
15
+ const graphQL = provider;
16
+ const url = graphQL?.ConfigData?.URL;
17
+ if (!graphQL || !url) {
18
+ return null;
19
+ }
20
+ return new FetchRecordingTransport({
21
+ BaseURL: RecordingBaseURLFromGraphQLURL(url),
22
+ Headers: () => {
23
+ const config = graphQL.ConfigData;
24
+ const headers = {};
25
+ if (config?.Token)
26
+ headers.authorization = `Bearer ${config.Token}`;
27
+ if (config?.MJAPIKey)
28
+ headers['x-mj-api-key'] = config.MJAPIKey;
29
+ if (config?.UserAPIKey)
30
+ headers['x-api-key'] = config.UserAPIKey;
31
+ return headers;
32
+ },
33
+ });
34
+ }
35
+ /**
36
+ * One drain pass over `userID`'s stored shards, oldest first, each under its own session and index. A
37
+ * shard the server acknowledges or refuses for good (see {@link RecordingUploadOutcome}) leaves the
38
+ * store; one that may still succeed stays for the next pass or load. Never rejects for an upload or
39
+ * delete failure (each is logged); a store whose listing throws rejects.
40
+ */
41
+ export async function DrainStoredRecordingShardsOnce(store, transport, userID) {
42
+ const pending = await store.ListOldest(MAX_DRAINED_SHARDS_PER_LOAD, userID);
43
+ let delivered = 0;
44
+ let refused = 0;
45
+ for (const shard of pending) {
46
+ let outcome = 'Retry';
47
+ try {
48
+ outcome = await transport.UploadShard(shard.SessionID, shard);
49
+ }
50
+ catch (error) {
51
+ console.warn(`[RealtimeSession] Re-uploading stored recording shard ${shard.SessionID}/${shard.Index} failed: ${describeFailure(error)}; it stays stored for the next load.`, error);
52
+ }
53
+ if (outcome === 'Acked') {
54
+ await DeleteStoredRecordingShard(store, shard.SessionID, shard.Index);
55
+ delivered++;
56
+ }
57
+ else if (outcome === 'Refused') {
58
+ console.warn(`[RealtimeSession] The server refused stored recording shard ${shard.SessionID}/${shard.Index} for good; deleting it so it no longer holds back the shards behind it.`);
59
+ await DeleteStoredRecordingShard(store, shard.SessionID, shard.Index);
60
+ refused++;
61
+ }
62
+ }
63
+ if (pending.length > 0) {
64
+ console.log(`[RealtimeSession] Re-uploaded ${delivered}/${pending.length} stored recording shard(s); ${refused} refused and deleted.`);
65
+ }
66
+ return { Listed: pending.length, Delivered: delivered, Refused: refused };
67
+ }
68
+ /** Removes one shard from the store. Never rejects: a failed delete only costs a duplicate re-upload. */
69
+ export async function DeleteStoredRecordingShard(store, sessionId, index) {
70
+ try {
71
+ await store.Delete(sessionId, index);
72
+ }
73
+ catch (error) {
74
+ // Harmless beyond a wasted re-upload on the next load: the server acks a duplicate.
75
+ console.warn(`[RealtimeSession] Failed to delete recording shard ${sessionId}/${index} from the local store:`, error);
76
+ }
77
+ }
78
+ /** Most drain passes a logout runs before it deletes what is left (each pass is itself bounded). */
79
+ const MAX_LOGOUT_DRAIN_PASSES = 3;
80
+ /**
81
+ * For logout: tries to deliver `userID`'s stored shards within `timeoutMs`, then deletes every shard
82
+ * of that user that is still stored, so their call audio neither stays on a shared browser's disk nor
83
+ * waits for a later login. Other users' shards are untouched. Never rejects and never takes longer
84
+ * than `timeoutMs` plus the delete.
85
+ */
86
+ export async function ReleaseUserRecordingShards(store, transport, userID, timeoutMs) {
87
+ let delivered = 0;
88
+ let timedOut = false;
89
+ if (transport) {
90
+ let timer;
91
+ const deadline = new Promise((resolve) => {
92
+ timer = setTimeout(() => resolve('timeout'), timeoutMs);
93
+ });
94
+ const attempt = (async () => {
95
+ for (let pass = 1; pass <= MAX_LOGOUT_DRAIN_PASSES; pass++) {
96
+ const result = await DrainStoredRecordingShardsOnce(store, transport, userID);
97
+ delivered += result.Delivered;
98
+ // Another pass only reaches shards beyond this pass's page, and only when this one made room;
99
+ // a shard that answered Retry is not retried within the logout.
100
+ if (result.Listed < MAX_DRAINED_SHARDS_PER_LOAD || result.Delivered + result.Refused === 0) {
101
+ return;
102
+ }
103
+ }
104
+ })().catch((error) => {
105
+ console.warn(`[RealtimeSession] Sending stored recording shards at logout failed for user ${userID ?? '(none)'}: ${describeFailure(error)}; deleting them.`, error);
106
+ });
107
+ try {
108
+ timedOut = (await Promise.race([attempt, deadline])) === 'timeout';
109
+ }
110
+ finally {
111
+ clearTimeout(timer);
112
+ }
113
+ if (timedOut) {
114
+ console.warn(`[RealtimeSession] Sending stored recording shards at logout took over ${timeoutMs} ms for user ${userID ?? '(none)'}; deleting what is left.`);
115
+ }
116
+ }
117
+ try {
118
+ await store.DeleteForUser(userID);
119
+ }
120
+ catch (error) {
121
+ console.warn(`[RealtimeSession] Failed to delete the stored recording shards of user ${userID ?? '(none)'} at logout:`, error);
122
+ }
123
+ return { Delivered: delivered, TimedOut: timedOut };
124
+ }
125
+ /** How long a logout waits for stored shards to upload before it deletes them. */
126
+ export const RECORDING_LOGOUT_SEND_TIMEOUT_MS = 3000;
127
+ /**
128
+ * A host's logout step: {@link ReleaseUserRecordingShards} for the provider's signed-in user, with the
129
+ * browser's IndexedDB store and the provider's recording transport. Call it while the provider is
130
+ * still authenticated (before its token is dropped). Never rejects.
131
+ */
132
+ export function ReleaseRecordingShardsAtLogout(provider, timeoutMs = RECORDING_LOGOUT_SEND_TIMEOUT_MS) {
133
+ return ReleaseUserRecordingShards(CreateRecordingShardStore(globalThis.indexedDB), CreateRecordingTransportForProvider(provider), provider?.CurrentUser?.ID ?? null, timeoutMs);
134
+ }
135
+ //# sourceMappingURL=RecordingShardDrain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordingShardDrain.js","sourceRoot":"","sources":["../../src/session/RecordingShardDrain.ts"],"names":[],"mappings":"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"}
@@ -1,76 +1,102 @@
1
1
  /**
2
- * @fileoverview Retry queue for crash-recovery audio shards.
2
+ * @fileoverview Retry queue for indexed recording shards.
3
3
  *
4
- * The browser recorder is snapshotted every ~15 s and each snapshot ("shard") is uploaded so a tab
5
- * crash loses at most one window of audio. Before this queue a failed upload was never retried, so a
6
- * single transient error left a permanent gap in the recovered recording (#5197). The queue keeps
7
- * failed shards and resends them, oldest first, on later flushes — bounded so a dead server cannot
8
- * grow memory without limit.
4
+ * The recorder cuts the call's Opus stream into already-indexed `MJOPUS1` shards (see
5
+ * {@link RecordingShardWindow}) and each one is uploaded as it seals. Before this queue a failed
6
+ * upload was never retried, so a single transient error left a permanent gap in the recovered
7
+ * recording (#5197). The queue keeps failed shards and resends them, oldest first, on later
8
+ * flushes — bounded so a dead server cannot grow memory without limit. A shard the queue gives up on
9
+ * is not lost: it stays in the IndexedDB store, and the owner is told so it can re-send it from there
10
+ * once uploads succeed again (or a later load drains it).
11
+ *
12
+ * The queue is told why an upload failed, not only that it did (#6012): a `Retry` outcome (dead
13
+ * network, 5xx, expired token) is retried, while a `Refused` outcome is final and the shard is given
14
+ * up on at once — the transport's contract says re-sending it is pointless.
9
15
  *
10
16
  * @module @memberjunction/realtime-runtime
11
17
  */
12
- /** Takes the audio recorded since the previous snapshot as base64, or null/'' when there is none. */
13
- export type ShardSnapshot = () => Promise<string | null>;
14
- /** Resolves true when the server stored the shard; false or a throw are both failures. */
15
- export type ShardUpload = (segmentIndex: number, audioBase64: string) => Promise<boolean>;
18
+ import type { RecordingShard } from './RecordingShardWindow.js';
19
+ import type { RecordingUploadOutcome } from './RecordingTransport.js';
20
+ /**
21
+ * Sends one shard and resolves the transport's outcome: `Acked` (stored), `Retry` (may succeed later;
22
+ * retried up to {@link MAX_SHARD_UPLOAD_ATTEMPTS}), or `Refused` (final; given up on after this one
23
+ * attempt). A throw counts as `Retry`, with the thrown error as the logged reason.
24
+ */
25
+ export type ShardUpload = (shard: RecordingShard) => Promise<RecordingUploadOutcome>;
26
+ /**
27
+ * Told about shards the queue stops retrying while the recording is live (attempts used up, the
28
+ * server refused the shard for good, or the retry list overflowed). They are still in the store; the owner re-sends them once uploads work.
29
+ */
30
+ export type ShardsGivenUp = (shards: RecordingShard[]) => void;
16
31
  /** Total upload attempts per shard (the first try plus retries) before it is dropped. */
17
32
  export declare const MAX_SHARD_UPLOAD_ATTEMPTS = 3;
18
33
  /**
19
- * Flushes allowed in flight at once. Ticks are 15 s apart, so overlap only happens when an upload is
20
- * slower than a tick; an unbounded pile-up on a dead network would snapshot (and hold in memory) a
21
- * new window every tick. A skipped tick costs nothing: the recorder keeps the audio and the next
22
- * flush ships the larger window. While both slots are pinned the recorder cursor does not advance,
23
- * so the next capture is one larger window (bounded by the upload timeout x attempts); if that
24
- * shard is later dropped, recovery fills a correspondingly longer gap with silence.
34
+ * Flushes allowed in flight at once. Flushes are ~5 s apart, so overlap only happens when an upload
35
+ * is slower than a tick; an unbounded pile-up on a dead network would start a new batch of uploads
36
+ * every tick. A flush that arrives at capacity does not upload, but it does not lose its shards
37
+ * either: they are retained and the next flush that has a free slot sends them, oldest first.
25
38
  */
26
39
  export declare const MAX_CONCURRENT_FLUSHES = 2;
27
40
  /**
28
41
  * Per-attempt upload deadline. A hung request would otherwise pin one of the
29
42
  * {@link MAX_CONCURRENT_FLUSHES} slots forever and silently stop all later shards. A timeout counts
30
43
  * as a failed attempt; if the request later succeeds anyway that is harmless, because the server
31
- * key is per-index and a resend is an idempotent overwrite. The timeout is logical only:
32
- * `ExecuteGQL` accepts no AbortSignal, so a timed-out request may still be on the wire. Requests
33
- * stay rate-bounded regardless (at most {@link MAX_CONCURRENT_FLUSHES} flushes, each sending at
34
- * most one batch, per timeout window), and a late success is an idempotent same-index overwrite.
44
+ * key is per-index and a resend of the same bytes is an idempotent overwrite. The timeout is
45
+ * logical only: the transport may accept no AbortSignal, so a timed-out request may still be on the
46
+ * wire. Requests stay rate-bounded regardless (at most {@link MAX_CONCURRENT_FLUSHES} flushes, each
47
+ * sending at most one batch, per timeout window).
35
48
  */
36
49
  export declare const SHARD_UPLOAD_TIMEOUT_MS = 60000;
37
50
  /**
38
- * Assigns chronological indexes to crash-recovery shards, uploads them, and retains failures for
39
- * retry on later flushes. Safe to call {@link Flush} from an overlapping timer.
51
+ * Cap on shards held for retry: 10 minutes of 5 s shards. A dead network with flushes at capacity
52
+ * would otherwise grow the retained list (and its bytes) without bound. Overflow drops the oldest;
53
+ * dropped shards remain in the IndexedDB store for the next-load drain.
54
+ */
55
+ export declare const MAX_RETAINED_SHARDS = 120;
56
+ /**
57
+ * Uploads indexed shards and retains failures for retry on later flushes. A retry keeps its shard's
58
+ * index and bytes. Safe to call {@link Flush} from an overlapping timer.
40
59
  */
41
60
  export declare class RecordingShardQueue {
42
- private nextIndex;
61
+ private readonly upload;
62
+ private readonly onGivenUp;
43
63
  private retained;
64
+ /** Every shard handed to the queue and not yet acknowledged or given up on, by index. */
65
+ private readonly outstanding;
44
66
  private inFlight;
45
67
  /** True once a capacity stall has been logged; cleared when a flush actually runs. */
46
68
  private capacityWarned;
47
- /** Set by {@link Close} when the recording ends; nothing is snapshotted, sent or retained after it. */
69
+ /** Set by {@link Close} when the recording ends; nothing is sent or retained after it. */
48
70
  private closed;
49
71
  /**
50
- * Serializes "take a snapshot, then assign an index". The recorder advances its cursor
51
- * synchronously but encodes asynchronously, so two overlapping snapshots could resolve out of
52
- * order and give newer audio an older index. Never rejected: the link catches internally.
72
+ * @param upload sends one shard
73
+ * @param onGivenUp told about every shard dropped while the recording is live (not on {@link Close})
53
74
  */
54
- private captureChain;
75
+ constructor(upload: ShardUpload, onGivenUp?: ShardsGivenUp);
55
76
  /** Shards awaiting a retry (for tests and diagnostics). */
56
77
  get RetainedCount(): number;
57
78
  /**
58
- * Takes ownership of retained shards and uploads them oldest-first, then captures a new window
59
- * and uploads it. Retained shards go first so a hung snapshot cannot strand them. A failure never
60
- * stops the rest of the flush. Skips entirely (no snapshot) when
61
- * {@link MAX_CONCURRENT_FLUSHES} are already running. Never rejects.
79
+ * Every shard the queue still holds without an acknowledgement — on the wire, waiting in a flush's
80
+ * batch, or retained for a retry — oldest first. A page that is about to unload re-sends these with
81
+ * `keepalive`, because its ordinary requests are aborted with it.
82
+ */
83
+ Unacknowledged(): RecordingShard[];
84
+ /**
85
+ * Uploads retained shards oldest-first, then `fresh`. A failure never stops the rest of the
86
+ * flush. At {@link MAX_CONCURRENT_FLUSHES} in flight nothing is uploaded and `fresh` is retained
87
+ * for the next flush instead. Never rejects.
62
88
  */
63
- Flush(snapshot: ShardSnapshot, upload: ShardUpload): Promise<void>;
89
+ Flush(fresh: RecordingShard[]): Promise<void>;
64
90
  /**
65
91
  * Ends the queue with its recording: discards retained shards and stops any flush in flight from
66
- * sending more. An upload already on the wire cannot be aborted (`ExecuteGQL` takes no
67
- * AbortSignal), so at most that one request can still land.
92
+ * sending more. An upload already on the wire cannot be aborted, so at most that one request can
93
+ * still land. Discarded shards remain in the IndexedDB store and are drained on a later load.
68
94
  */
69
95
  Close(): void;
70
- /** Warns once per stall so a wedged uploader is visible without logging every 15 s tick. */
96
+ /** Sorted re-insert: concurrent flushes can fail out of index order, and the oldest must go first. */
97
+ private retain;
98
+ /** Warns once per stall so a wedged uploader is visible without logging every tick. */
71
99
  private warnCapacitySkip;
72
- private captureShard;
73
- private takeIndexedSnapshot;
74
100
  private uploadWithTimeout;
75
101
  private send;
76
102
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RecordingShardQueue.d.ts","sourceRoot":"","sources":["../../src/session/RecordingShardQueue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,qGAAqG;AACrG,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEzD,0FAA0F;AAC1F,MAAM,MAAM,WAAW,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAE1F,yFAAyF;AACzF,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAa9C;;;GAGG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,QAAQ,CAAK;IACrB,sFAAsF;IACtF,OAAO,CAAC,cAAc,CAAS;IAC/B,uGAAuG;IACvG,OAAO,CAAC,MAAM,CAAS;IACvB;;;;OAIG;IACH,OAAO,CAAC,YAAY,CAAoC;IAExD,2DAA2D;IAC3D,IAAW,aAAa,IAAI,MAAM,CAEjC;IAED;;;;;OAKG;IACU,KAAK,CAAC,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IA+B/E;;;;OAIG;IACI,KAAK,IAAI,IAAI;IAKpB,4FAA4F;IAC5F,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,YAAY;YAMN,mBAAmB;YAanB,iBAAiB;YAYjB,IAAI;CAkCnB"}
1
+ {"version":3,"file":"RecordingShardQueue.d.ts","sourceRoot":"","sources":["../../src/session/RecordingShardQueue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEnE;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAarF;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;AAE/D,yFAAyF;AACzF,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAYvC;;;GAGG;AACH,qBAAa,mBAAmB;IAclB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAe,OAAO,CAAC,QAAQ,CAAC,SAAS;IAb5E,OAAO,CAAC,QAAQ,CAAsB;IACtC,yFAAyF;IACzF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;IACjE,OAAO,CAAC,QAAQ,CAAK;IACrB,sFAAsF;IACtF,OAAO,CAAC,cAAc,CAAS;IAC/B,0FAA0F;IAC1F,OAAO,CAAC,MAAM,CAAS;IAEvB;;;OAGG;gBAC0B,MAAM,EAAE,WAAW,EAAmB,SAAS,GAAE,aAA+B;IAE7G,2DAA2D;IAC3D,IAAW,aAAa,IAAI,MAAM,CAEjC;IAED;;;;OAIG;IACI,cAAc,IAAI,cAAc,EAAE;IAIzC;;;;OAIG;IACU,KAAK,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC1D;;;;OAIG;IACI,KAAK,IAAI,IAAI;IAMpB,sGAAsG;IACtG,OAAO,CAAC,MAAM;IAgBd,uFAAuF;IACvF,OAAO,CAAC,gBAAgB;YAUV,iBAAiB;YAYjB,IAAI;CA+CnB"}
@@ -1,71 +1,92 @@
1
1
  /**
2
- * @fileoverview Retry queue for crash-recovery audio shards.
3
- *
4
- * The browser recorder is snapshotted every ~15 s and each snapshot ("shard") is uploaded so a tab
5
- * crash loses at most one window of audio. Before this queue a failed upload was never retried, so a
6
- * single transient error left a permanent gap in the recovered recording (#5197). The queue keeps
7
- * failed shards and resends them, oldest first, on later flushes — bounded so a dead server cannot
8
- * grow memory without limit.
9
- *
10
- * @module @memberjunction/realtime-runtime
2
+ * Warn reason for a `Retry` outcome with no thrown error. A real transport has already logged the
3
+ * status or fetch error on its own line (with no transport at all, the runtime warned once at start),
4
+ * so this only must not read as a server refusal. It carries no "will be retried": the same reason
5
+ * ends the give-up line, where that would be false.
11
6
  */
7
+ const RETRY_REASON = 'transport or server error, not a refusal';
8
+ /** Warn reason for a `Refused` outcome. */
9
+ const REFUSED_REASON = 'server refused the shard for good';
12
10
  /** Total upload attempts per shard (the first try plus retries) before it is dropped. */
13
11
  export const MAX_SHARD_UPLOAD_ATTEMPTS = 3;
14
12
  /**
15
- * Flushes allowed in flight at once. Ticks are 15 s apart, so overlap only happens when an upload is
16
- * slower than a tick; an unbounded pile-up on a dead network would snapshot (and hold in memory) a
17
- * new window every tick. A skipped tick costs nothing: the recorder keeps the audio and the next
18
- * flush ships the larger window. While both slots are pinned the recorder cursor does not advance,
19
- * so the next capture is one larger window (bounded by the upload timeout x attempts); if that
20
- * shard is later dropped, recovery fills a correspondingly longer gap with silence.
13
+ * Flushes allowed in flight at once. Flushes are ~5 s apart, so overlap only happens when an upload
14
+ * is slower than a tick; an unbounded pile-up on a dead network would start a new batch of uploads
15
+ * every tick. A flush that arrives at capacity does not upload, but it does not lose its shards
16
+ * either: they are retained and the next flush that has a free slot sends them, oldest first.
21
17
  */
22
18
  export const MAX_CONCURRENT_FLUSHES = 2;
23
19
  /**
24
20
  * Per-attempt upload deadline. A hung request would otherwise pin one of the
25
21
  * {@link MAX_CONCURRENT_FLUSHES} slots forever and silently stop all later shards. A timeout counts
26
22
  * as a failed attempt; if the request later succeeds anyway that is harmless, because the server
27
- * key is per-index and a resend is an idempotent overwrite. The timeout is logical only:
28
- * `ExecuteGQL` accepts no AbortSignal, so a timed-out request may still be on the wire. Requests
29
- * stay rate-bounded regardless (at most {@link MAX_CONCURRENT_FLUSHES} flushes, each sending at
30
- * most one batch, per timeout window), and a late success is an idempotent same-index overwrite.
23
+ * key is per-index and a resend of the same bytes is an idempotent overwrite. The timeout is
24
+ * logical only: the transport may accept no AbortSignal, so a timed-out request may still be on the
25
+ * wire. Requests stay rate-bounded regardless (at most {@link MAX_CONCURRENT_FLUSHES} flushes, each
26
+ * sending at most one batch, per timeout window).
31
27
  */
32
28
  export const SHARD_UPLOAD_TIMEOUT_MS = 60_000;
29
+ /**
30
+ * Cap on shards held for retry: 10 minutes of 5 s shards. A dead network with flushes at capacity
31
+ * would otherwise grow the retained list (and its bytes) without bound. Overflow drops the oldest;
32
+ * dropped shards remain in the IndexedDB store for the next-load drain.
33
+ */
34
+ export const MAX_RETAINED_SHARDS = 120;
33
35
  /** One-line reason for a warn message; the raw value is still passed along for its stack. */
34
36
  function describeFailure(failure) {
35
37
  return failure instanceof Error ? failure.message : String(failure);
36
38
  }
37
39
  /**
38
- * Assigns chronological indexes to crash-recovery shards, uploads them, and retains failures for
39
- * retry on later flushes. Safe to call {@link Flush} from an overlapping timer.
40
+ * Uploads indexed shards and retains failures for retry on later flushes. A retry keeps its shard's
41
+ * index and bytes. Safe to call {@link Flush} from an overlapping timer.
40
42
  */
41
43
  export class RecordingShardQueue {
42
- constructor() {
43
- this.nextIndex = 0;
44
+ /**
45
+ * @param upload sends one shard
46
+ * @param onGivenUp told about every shard dropped while the recording is live (not on {@link Close})
47
+ */
48
+ constructor(upload, onGivenUp = () => undefined) {
49
+ this.upload = upload;
50
+ this.onGivenUp = onGivenUp;
44
51
  this.retained = [];
52
+ /** Every shard handed to the queue and not yet acknowledged or given up on, by index. */
53
+ this.outstanding = new Map();
45
54
  this.inFlight = 0;
46
55
  /** True once a capacity stall has been logged; cleared when a flush actually runs. */
47
56
  this.capacityWarned = false;
48
- /** Set by {@link Close} when the recording ends; nothing is snapshotted, sent or retained after it. */
57
+ /** Set by {@link Close} when the recording ends; nothing is sent or retained after it. */
49
58
  this.closed = false;
50
- /**
51
- * Serializes "take a snapshot, then assign an index". The recorder advances its cursor
52
- * synchronously but encodes asynchronously, so two overlapping snapshots could resolve out of
53
- * order and give newer audio an older index. Never rejected: the link catches internally.
54
- */
55
- this.captureChain = Promise.resolve();
56
59
  }
57
60
  /** Shards awaiting a retry (for tests and diagnostics). */
58
61
  get RetainedCount() {
59
62
  return this.retained.length;
60
63
  }
61
64
  /**
62
- * Takes ownership of retained shards and uploads them oldest-first, then captures a new window
63
- * and uploads it. Retained shards go first so a hung snapshot cannot strand them. A failure never
64
- * stops the rest of the flush. Skips entirely (no snapshot) when
65
- * {@link MAX_CONCURRENT_FLUSHES} are already running. Never rejects.
65
+ * Every shard the queue still holds without an acknowledgement — on the wire, waiting in a flush's
66
+ * batch, or retained for a retry — oldest first. A page that is about to unload re-sends these with
67
+ * `keepalive`, because its ordinary requests are aborted with it.
68
+ */
69
+ Unacknowledged() {
70
+ return [...this.outstanding.values()].sort((a, b) => a.Index - b.Index);
71
+ }
72
+ /**
73
+ * Uploads retained shards oldest-first, then `fresh`. A failure never stops the rest of the
74
+ * flush. At {@link MAX_CONCURRENT_FLUSHES} in flight nothing is uploaded and `fresh` is retained
75
+ * for the next flush instead. Never rejects.
66
76
  */
67
- async Flush(snapshot, upload) {
77
+ async Flush(fresh) {
78
+ if (this.closed) {
79
+ if (fresh.length > 0) {
80
+ console.warn(`[RealtimeSession] Recording flush after the recording ended; shard(s) ${fresh.map((s) => s.Index).join(', ')} not sent — they stay in the local store and are drained later.`);
81
+ }
82
+ return;
83
+ }
84
+ const freshPending = fresh.map((shard) => ({ Shard: shard, Attempts: 0 }));
85
+ for (const shard of fresh) {
86
+ this.outstanding.set(shard.Index, shard);
87
+ }
68
88
  if (this.inFlight >= MAX_CONCURRENT_FLUSHES) {
89
+ this.retain(freshPending);
69
90
  this.warnCapacitySkip();
70
91
  return;
71
92
  }
@@ -73,22 +94,15 @@ export class RecordingShardQueue {
73
94
  this.inFlight++;
74
95
  try {
75
96
  // Swap the array out so a concurrent flush only sees shards nobody is currently sending.
76
- const batch = this.retained;
97
+ const batch = [...this.retained, ...freshPending].sort((a, b) => a.Shard.Index - b.Shard.Index);
77
98
  this.retained = [];
78
- // Retained shards go first, BEFORE the capture: a snapshot that never resolves must not strand
79
- // shards that already failed. Index order is unaffected — indexes are assigned in capture order
80
- // on the serialized chain, and every retained shard already has an older index.
81
- // `closed` is re-checked before every send: a flush running when the recording ends must not
82
- // keep uploading, or a resend could land after the end-of-call upload has deleted the shards.
83
- for (const shard of batch) {
99
+ for (const pending of batch) {
100
+ // Re-checked before every send: a flush running when the recording ends must not keep
101
+ // uploading shards the terminal request is about to supersede.
84
102
  if (this.closed) {
85
103
  return;
86
104
  }
87
- await this.send(shard, upload);
88
- }
89
- const fresh = this.closed ? null : await this.captureShard(snapshot);
90
- if (fresh && !this.closed) {
91
- await this.send(fresh, upload);
105
+ await this.send(pending);
92
106
  }
93
107
  }
94
108
  finally {
@@ -97,76 +111,84 @@ export class RecordingShardQueue {
97
111
  }
98
112
  /**
99
113
  * Ends the queue with its recording: discards retained shards and stops any flush in flight from
100
- * sending more. An upload already on the wire cannot be aborted (`ExecuteGQL` takes no
101
- * AbortSignal), so at most that one request can still land.
114
+ * sending more. An upload already on the wire cannot be aborted, so at most that one request can
115
+ * still land. Discarded shards remain in the IndexedDB store and are drained on a later load.
102
116
  */
103
117
  Close() {
104
118
  this.closed = true;
105
119
  this.retained = [];
120
+ this.outstanding.clear();
106
121
  }
107
- /** Warns once per stall so a wedged uploader is visible without logging every 15 s tick. */
122
+ /** Sorted re-insert: concurrent flushes can fail out of index order, and the oldest must go first. */
123
+ retain(pending) {
124
+ this.retained.push(...pending);
125
+ this.retained.sort((a, b) => a.Shard.Index - b.Shard.Index);
126
+ const overflow = this.retained.length - MAX_RETAINED_SHARDS;
127
+ if (overflow > 0) {
128
+ const dropped = this.retained.splice(0, overflow);
129
+ for (const pending of dropped) {
130
+ this.outstanding.delete(pending.Shard.Index);
131
+ }
132
+ console.warn(`[RealtimeSession] Retry queue over ${MAX_RETAINED_SHARDS} shards; dropped oldest shard(s) ${dropped.map((p) => p.Shard.Index).join(', ')} from memory — they stay in the local IndexedDB store and are re-sent from it once uploads succeed again.`);
133
+ this.onGivenUp(dropped.map((p) => p.Shard));
134
+ }
135
+ }
136
+ /** Warns once per stall so a wedged uploader is visible without logging every tick. */
108
137
  warnCapacitySkip() {
109
138
  if (this.capacityWarned) {
110
139
  return;
111
140
  }
112
141
  this.capacityWarned = true;
113
- console.warn(`[RealtimeSession] Recording flush skipped: at capacity with ${this.inFlight}/${MAX_CONCURRENT_FLUSHES} flushes in flight and ${this.retained.length} shard(s) retained. The recorder keeps the audio for the next flush.`);
114
- }
115
- captureShard(snapshot) {
116
- const capture = this.captureChain.then(() => this.takeIndexedSnapshot(snapshot));
117
- this.captureChain = capture.then(() => undefined);
118
- return capture;
119
- }
120
- async takeIndexedSnapshot(snapshot) {
121
- try {
122
- const audio = await snapshot();
123
- if (!audio) {
124
- return null;
125
- }
126
- return { Index: this.nextIndex++, AudioBase64: audio, Attempts: 0 };
127
- }
128
- catch (error) {
129
- console.warn('[RealtimeSession] Failed to snapshot recording shard:', error);
130
- return null;
131
- }
142
+ console.warn(`[RealtimeSession] Recording flush at capacity: ${this.inFlight}/${MAX_CONCURRENT_FLUSHES} flushes in flight, ${this.retained.length} shard(s) retained. Retained shards are sent by the next flush with a free slot.`);
132
143
  }
133
- async uploadWithTimeout(upload, shard) {
144
+ async uploadWithTimeout(shard) {
134
145
  let timer;
135
146
  const deadline = new Promise((_, reject) => {
136
147
  timer = setTimeout(() => reject(new Error(`upload timed out after ${SHARD_UPLOAD_TIMEOUT_MS}ms`)), SHARD_UPLOAD_TIMEOUT_MS);
137
148
  });
138
149
  try {
139
- return await Promise.race([upload(shard.Index, shard.AudioBase64), deadline]);
150
+ return await Promise.race([this.upload(shard), deadline]);
140
151
  }
141
152
  finally {
142
153
  clearTimeout(timer);
143
154
  }
144
155
  }
145
- async send(shard, upload) {
146
- shard.Attempts++;
147
- let stored = false;
148
- let failure = 'server refused the shard';
156
+ async send(pending) {
157
+ const { Shard: shard } = pending;
158
+ pending.Attempts++;
159
+ let outcome;
160
+ let failure;
149
161
  try {
150
- stored = await this.uploadWithTimeout(upload, shard);
162
+ outcome = await this.uploadWithTimeout(shard);
163
+ failure = outcome === 'Refused' ? REFUSED_REASON : RETRY_REASON;
151
164
  }
152
165
  catch (error) {
166
+ outcome = 'Retry';
153
167
  failure = error;
154
168
  }
155
- if (stored) {
169
+ if (outcome === 'Acked') {
170
+ this.outstanding.delete(shard.Index);
156
171
  return;
157
172
  }
158
173
  if (this.closed) {
159
- console.warn(`[RealtimeSession] Recording shard ${shard.Index} upload failed after the recording ended (${describeFailure(failure)}); not retried — the end-of-call upload supersedes it:`, failure);
174
+ console.warn(`[RealtimeSession] Recording shard ${shard.Index} upload failed after the recording ended (${describeFailure(failure)}); not retried — it stays in the local store and is drained later:`, failure);
175
+ return;
176
+ }
177
+ // Checked before the attempts cap, so a refusal on the last attempt still reads as a refusal.
178
+ if (outcome === 'Refused') {
179
+ console.warn(`[RealtimeSession] Recording shard ${shard.Index} upload failed (${REFUSED_REASON}); it will not be re-sent by the queue — it stays in the local store until a store drain settles it.`);
180
+ this.outstanding.delete(shard.Index);
181
+ this.onGivenUp([shard]);
160
182
  return;
161
183
  }
162
- if (shard.Attempts >= MAX_SHARD_UPLOAD_ATTEMPTS) {
163
- console.warn(`[RealtimeSession] Dropping recording shard ${shard.Index} after ${shard.Attempts}/${MAX_SHARD_UPLOAD_ATTEMPTS} failed uploads (${describeFailure(failure)}); recovery will fill the gap with silence:`, failure);
184
+ if (pending.Attempts >= MAX_SHARD_UPLOAD_ATTEMPTS) {
185
+ console.warn(`[RealtimeSession] Dropping recording shard ${shard.Index} from the retry queue after ${pending.Attempts}/${MAX_SHARD_UPLOAD_ATTEMPTS} failed uploads (${describeFailure(failure)}); it stays in the local store and is re-sent from it once uploads succeed again:`, failure);
186
+ this.outstanding.delete(shard.Index);
187
+ this.onGivenUp([shard]);
164
188
  return;
165
189
  }
166
- console.warn(`[RealtimeSession] Recording shard ${shard.Index} upload failed (attempt ${shard.Attempts}/${MAX_SHARD_UPLOAD_ATTEMPTS}: ${describeFailure(failure)}); will retry next flush:`, failure);
167
- // Sorted re-insert: concurrent flushes can fail out of index order, and the oldest must go first.
168
- this.retained.push(shard);
169
- this.retained.sort((a, b) => a.Index - b.Index);
190
+ console.warn(`[RealtimeSession] Recording shard ${shard.Index} upload failed (attempt ${pending.Attempts}/${MAX_SHARD_UPLOAD_ATTEMPTS}: ${describeFailure(failure)}); will retry next flush:`, failure);
191
+ this.retain([pending]);
170
192
  }
171
193
  }
172
194
  //# sourceMappingURL=RecordingShardQueue.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RecordingShardQueue.js","sourceRoot":"","sources":["../../src/session/RecordingShardQueue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,yFAAyF;AACzF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C,6FAA6F;AAC7F,SAAS,eAAe,CAAC,OAAgB;IACvC,OAAO,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACtE,CAAC;AAQD;;;GAGG;AACH,MAAM,OAAO,mBAAmB;IAAhC;QACU,cAAS,GAAG,CAAC,CAAC;QACd,aAAQ,GAAmB,EAAE,CAAC;QAC9B,aAAQ,GAAG,CAAC,CAAC;QACrB,sFAAsF;QAC9E,mBAAc,GAAG,KAAK,CAAC;QAC/B,uGAAuG;QAC/F,WAAM,GAAG,KAAK,CAAC;QACvB;;;;WAIG;QACK,iBAAY,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAkI1D,CAAC;IAhIC,2DAA2D;IAC3D,IAAW,aAAa;QACtB,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,KAAK,CAAC,QAAuB,EAAE,MAAmB;QAC7D,IAAI,IAAI,CAAC,QAAQ,IAAI,sBAAsB,EAAE,CAAC;YAC5C,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;QAC5B,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,IAAI,CAAC;YACH,yFAAyF;YACzF,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC5B,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;YACnB,+FAA+F;YAC/F,gGAAgG;YAChG,gFAAgF;YAChF,6FAA6F;YAC7F,8FAA8F;YAC9F,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;gBAC1B,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;oBAChB,OAAO;gBACT,CAAC;gBACD,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACjC,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;YACrE,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC1B,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACI,KAAK;QACV,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;IACrB,CAAC;IAED,4FAA4F;IACpF,gBAAgB;QACtB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,OAAO,CAAC,IAAI,CACV,+DAA+D,IAAI,CAAC,QAAQ,IAAI,sBAAsB,0BAA0B,IAAI,CAAC,QAAQ,CAAC,MAAM,sEAAsE,CAC3N,CAAC;IACJ,CAAC;IAEO,YAAY,CAAC,QAAuB;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC,CAAC;QACjF,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAClD,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,KAAK,CAAC,mBAAmB,CAAC,QAAuB;QACvD,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QACtE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,uDAAuD,EAAE,KAAK,CAAC,CAAC;YAC7E,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAAC,MAAmB,EAAE,KAAmB;QACtE,IAAI,KAAgD,CAAC;QACrD,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;YAChD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,uBAAuB,IAAI,CAAC,CAAC,EAAE,uBAAuB,CAAC,CAAC;QAC9H,CAAC,CAAC,CAAC;QACH,IAAI,CAAC;YACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QAChF,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,IAAI,CAAC,KAAmB,EAAE,MAAmB;QACzD,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjB,IAAI,MAAM,GAAG,KAAK,CAAC;QACnB,IAAI,OAAO,GAAY,0BAA0B,CAAC;QAClD,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,GAAG,KAAK,CAAC;QAClB,CAAC;QACD,IAAI,MAAM,EAAE,CAAC;YACX,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CACV,qCAAqC,KAAK,CAAC,KAAK,6CAA6C,eAAe,CAAC,OAAO,CAAC,wDAAwD,EAC7K,OAAO,CACR,CAAC;YACF,OAAO;QACT,CAAC;QACD,IAAI,KAAK,CAAC,QAAQ,IAAI,yBAAyB,EAAE,CAAC;YAChD,OAAO,CAAC,IAAI,CACV,8CAA8C,KAAK,CAAC,KAAK,UAAU,KAAK,CAAC,QAAQ,IAAI,yBAAyB,oBAAoB,eAAe,CAAC,OAAO,CAAC,6CAA6C,EACvM,OAAO,CACR,CAAC;YACF,OAAO;QACT,CAAC;QACD,OAAO,CAAC,IAAI,CACV,qCAAqC,KAAK,CAAC,KAAK,2BAA2B,KAAK,CAAC,QAAQ,IAAI,yBAAyB,KAAK,eAAe,CAAC,OAAO,CAAC,2BAA2B,EAC9K,OAAO,CACR,CAAC;QACF,kGAAkG;QAClG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;CACF"}
1
+ {"version":3,"file":"RecordingShardQueue.js","sourceRoot":"","sources":["../../src/session/RecordingShardQueue.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,MAAM,YAAY,GAAG,0CAA0C,CAAC;AAEhE,2CAA2C;AAC3C,MAAM,cAAc,GAAG,mCAAmC,CAAC;AAQ3D,yFAAyF;AACzF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC,6FAA6F;AAC7F,SAAS,eAAe,CAAC,OAAgB;IACvC,OAAO,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACtE,CAAC;AAOD;;;GAGG;AACH,MAAM,OAAO,mBAAmB;IAU9B;;;OAGG;IACH,YAA6B,MAAmB,EAAmB,YAA2B,GAAG,EAAE,CAAC,SAAS;QAAhF,WAAM,GAAN,MAAM,CAAa;QAAmB,cAAS,GAAT,SAAS,CAAiC;QAbrG,aAAQ,GAAmB,EAAE,CAAC;QACtC,yFAAyF;QACxE,gBAAW,GAAG,IAAI,GAAG,EAA0B,CAAC;QACzD,aAAQ,GAAG,CAAC,CAAC;QACrB,sFAAsF;QAC9E,mBAAc,GAAG,KAAK,CAAC;QAC/B,0FAA0F;QAClF,WAAM,GAAG,KAAK,CAAC;IAMyF,CAAC;IAEjH,2DAA2D;IAC3D,IAAW,aAAa;QACtB,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACI,cAAc;QACnB,OAAO,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IAC1E,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,KAAK,CAAC,KAAuB;QACxC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,OAAO,CAAC,IAAI,CACV,yEAAyE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,iEAAiE,CAC/K,CAAC;YACJ,CAAC;YACD,OAAO;QACT,CAAC;QACD,MAAM,YAAY,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAgB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACzF,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;YAC1B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,IAAI,sBAAsB,EAAE,CAAC;YAC5C,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;YAC1B,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;QAC5B,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,IAAI,CAAC;YACH,yFAAyF;YACzF,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAChG,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;YACnB,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;gBAC5B,sFAAsF;gBACtF,+DAA+D;gBAC/D,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;oBAChB,OAAO;gBACT,CAAC;gBACD,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACI,KAAK;QACV,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;IAED,sGAAsG;IAC9F,MAAM,CAAC,OAAuB;QACpC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;QAC/B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,mBAAmB,CAAC;QAC5D,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;YAClD,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;gBAC9B,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC/C,CAAC;YACD,OAAO,CAAC,IAAI,CACV,sCAAsC,mBAAmB,oCAAoC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,2GAA2G,CACrP,CAAC;YACF,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,uFAAuF;IAC/E,gBAAgB;QACtB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,OAAO,CAAC,IAAI,CACV,kDAAkD,IAAI,CAAC,QAAQ,IAAI,sBAAsB,uBAAuB,IAAI,CAAC,QAAQ,CAAC,MAAM,kFAAkF,CACvN,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAAC,KAAqB;QACnD,IAAI,KAAgD,CAAC;QACrD,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;YAChD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,uBAAuB,IAAI,CAAC,CAAC,EAAE,uBAAuB,CAAC,CAAC;QAC9H,CAAC,CAAC,CAAC;QACH,IAAI,CAAC;YACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC5D,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,IAAI,CAAC,OAAqB;QACtC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;QACjC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACnB,IAAI,OAA+B,CAAC;QACpC,IAAI,OAAgB,CAAC;QACrB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAC9C,OAAO,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC;QAClE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,GAAG,OAAO,CAAC;YAClB,OAAO,GAAG,KAAK,CAAC;QAClB,CAAC;QACD,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YACxB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrC,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CACV,qCAAqC,KAAK,CAAC,KAAK,6CAA6C,eAAe,CAAC,OAAO,CAAC,oEAAoE,EACzL,OAAO,CACR,CAAC;YACF,OAAO;QACT,CAAC;QACD,8FAA8F;QAC9F,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,IAAI,CACV,qCAAqC,KAAK,CAAC,KAAK,mBAAmB,cAAc,sGAAsG,CACxL,CAAC;YACF,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YACxB,OAAO;QACT,CAAC;QACD,IAAI,OAAO,CAAC,QAAQ,IAAI,yBAAyB,EAAE,CAAC;YAClD,OAAO,CAAC,IAAI,CACV,8CAA8C,KAAK,CAAC,KAAK,+BAA+B,OAAO,CAAC,QAAQ,IAAI,yBAAyB,oBAAoB,eAAe,CAAC,OAAO,CAAC,mFAAmF,EACpQ,OAAO,CACR,CAAC;YACF,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YACxB,OAAO;QACT,CAAC;QACD,OAAO,CAAC,IAAI,CACV,qCAAqC,KAAK,CAAC,KAAK,2BAA2B,OAAO,CAAC,QAAQ,IAAI,yBAAyB,KAAK,eAAe,CAAC,OAAO,CAAC,2BAA2B,EAChL,OAAO,CACR,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACzB,CAAC;CACF"}