@memberjunction/realtime-runtime 6.2.0-edge.3 → 6.2.0-edge.5

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 (114) hide show
  1. package/README.md +75 -0
  2. package/dist/channels/base-realtime-channel-client.d.ts +535 -28
  3. package/dist/channels/base-realtime-channel-client.d.ts.map +1 -1
  4. package/dist/channels/base-realtime-channel-client.js +864 -10
  5. package/dist/channels/base-realtime-channel-client.js.map +1 -1
  6. package/dist/channels/channel-action-dispatcher.d.ts +70 -0
  7. package/dist/channels/channel-action-dispatcher.d.ts.map +1 -0
  8. package/dist/channels/channel-action-dispatcher.js +136 -0
  9. package/dist/channels/channel-action-dispatcher.js.map +1 -0
  10. package/dist/channels/channel-catalog-note.d.ts +48 -0
  11. package/dist/channels/channel-catalog-note.d.ts.map +1 -0
  12. package/dist/channels/channel-catalog-note.js +119 -0
  13. package/dist/channels/channel-catalog-note.js.map +1 -0
  14. package/dist/channels/channel-contract-types.d.ts +107 -0
  15. package/dist/channels/channel-contract-types.d.ts.map +1 -0
  16. package/dist/channels/channel-contract-types.js +12 -0
  17. package/dist/channels/channel-contract-types.js.map +1 -0
  18. package/dist/channels/channel-descriptor-synthesis.d.ts +56 -0
  19. package/dist/channels/channel-descriptor-synthesis.d.ts.map +1 -0
  20. package/dist/channels/channel-descriptor-synthesis.js +90 -0
  21. package/dist/channels/channel-descriptor-synthesis.js.map +1 -0
  22. package/dist/channels/channel-perception.d.ts +82 -0
  23. package/dist/channels/channel-perception.d.ts.map +1 -0
  24. package/dist/channels/channel-perception.js +120 -0
  25. package/dist/channels/channel-perception.js.map +1 -0
  26. package/dist/channels/channel-schema-format.d.ts +17 -0
  27. package/dist/channels/channel-schema-format.d.ts.map +1 -0
  28. package/dist/channels/channel-schema-format.js +45 -0
  29. package/dist/channels/channel-schema-format.js.map +1 -0
  30. package/dist/channels/channel-state-delta.d.ts +59 -0
  31. package/dist/channels/channel-state-delta.d.ts.map +1 -0
  32. package/dist/channels/channel-state-delta.js +116 -0
  33. package/dist/channels/channel-state-delta.js.map +1 -0
  34. package/dist/channels/channel-surface-placement.d.ts +25 -0
  35. package/dist/channels/channel-surface-placement.d.ts.map +1 -0
  36. package/dist/channels/channel-surface-placement.js +29 -0
  37. package/dist/channels/channel-surface-placement.js.map +1 -0
  38. package/dist/channels/channel-visual-pump.d.ts +126 -0
  39. package/dist/channels/channel-visual-pump.d.ts.map +1 -0
  40. package/dist/channels/channel-visual-pump.js +174 -0
  41. package/dist/channels/channel-visual-pump.js.map +1 -0
  42. package/dist/hosts/IRealtimeMediaHost.d.ts +86 -34
  43. package/dist/hosts/IRealtimeMediaHost.d.ts.map +1 -1
  44. package/dist/hosts/IRealtimeMediaHost.js +2 -1
  45. package/dist/hosts/IRealtimeMediaHost.js.map +1 -1
  46. package/dist/index.d.ts +26 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +28 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/session/RealtimeSessionRuntime.d.ts +984 -111
  51. package/dist/session/RealtimeSessionRuntime.d.ts.map +1 -1
  52. package/dist/session/RealtimeSessionRuntime.js +2396 -381
  53. package/dist/session/RealtimeSessionRuntime.js.map +1 -1
  54. package/dist/session/RecordingShardDrain.d.ts +59 -0
  55. package/dist/session/RecordingShardDrain.d.ts.map +1 -0
  56. package/dist/session/RecordingShardDrain.js +135 -0
  57. package/dist/session/RecordingShardDrain.js.map +1 -0
  58. package/dist/session/RecordingShardQueue.d.ts +94 -0
  59. package/dist/session/RecordingShardQueue.d.ts.map +1 -0
  60. package/dist/session/RecordingShardQueue.js +176 -0
  61. package/dist/session/RecordingShardQueue.js.map +1 -0
  62. package/dist/session/RecordingShardStore.d.ts +57 -0
  63. package/dist/session/RecordingShardStore.d.ts.map +1 -0
  64. package/dist/session/RecordingShardStore.js +184 -0
  65. package/dist/session/RecordingShardStore.js.map +1 -0
  66. package/dist/session/RecordingShardWindow.d.ts +42 -0
  67. package/dist/session/RecordingShardWindow.d.ts.map +1 -0
  68. package/dist/session/RecordingShardWindow.js +88 -0
  69. package/dist/session/RecordingShardWindow.js.map +1 -0
  70. package/dist/session/RecordingTransport.d.ts +66 -0
  71. package/dist/session/RecordingTransport.d.ts.map +1 -0
  72. package/dist/session/RecordingTransport.js +61 -0
  73. package/dist/session/RecordingTransport.js.map +1 -0
  74. package/dist/session/app-client-tool-registry.d.ts +76 -0
  75. package/dist/session/app-client-tool-registry.d.ts.map +1 -0
  76. package/dist/session/app-client-tool-registry.js +100 -0
  77. package/dist/session/app-client-tool-registry.js.map +1 -0
  78. package/dist/session/avatar-notice.d.ts +43 -0
  79. package/dist/session/avatar-notice.d.ts.map +1 -0
  80. package/dist/session/avatar-notice.js +60 -0
  81. package/dist/session/avatar-notice.js.map +1 -0
  82. package/dist/session/call-end.d.ts +21 -0
  83. package/dist/session/call-end.d.ts.map +1 -0
  84. package/dist/session/call-end.js +19 -0
  85. package/dist/session/call-end.js.map +1 -0
  86. package/dist/session/channel-exposure-preferences.d.ts +70 -0
  87. package/dist/session/channel-exposure-preferences.d.ts.map +1 -0
  88. package/dist/session/channel-exposure-preferences.js +138 -0
  89. package/dist/session/channel-exposure-preferences.js.map +1 -0
  90. package/dist/session/channel-session-scope.d.ts +142 -0
  91. package/dist/session/channel-session-scope.d.ts.map +1 -0
  92. package/dist/session/channel-session-scope.js +128 -0
  93. package/dist/session/channel-session-scope.js.map +1 -0
  94. package/dist/session/client-session-deadline.d.ts +28 -0
  95. package/dist/session/client-session-deadline.d.ts.map +1 -0
  96. package/dist/session/client-session-deadline.js +50 -0
  97. package/dist/session/client-session-deadline.js.map +1 -0
  98. package/dist/session/realtime-captures.d.ts +236 -0
  99. package/dist/session/realtime-captures.d.ts.map +1 -0
  100. package/dist/session/realtime-captures.js +383 -0
  101. package/dist/session/realtime-captures.js.map +1 -0
  102. package/dist/session/realtime-microphone.d.ts +50 -0
  103. package/dist/session/realtime-microphone.d.ts.map +1 -0
  104. package/dist/session/realtime-microphone.js +51 -0
  105. package/dist/session/realtime-microphone.js.map +1 -0
  106. package/dist/session/session-event-hub.d.ts +72 -0
  107. package/dist/session/session-event-hub.d.ts.map +1 -0
  108. package/dist/session/session-event-hub.js +151 -0
  109. package/dist/session/session-event-hub.js.map +1 -0
  110. package/dist/session/session-launcher.d.ts +128 -0
  111. package/dist/session/session-launcher.d.ts.map +1 -0
  112. package/dist/session/session-launcher.js +160 -0
  113. package/dist/session/session-launcher.js.map +1 -0
  114. package/package.json +10 -9
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @fileoverview Retry queue for indexed recording shards.
3
+ *
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
+ * @module @memberjunction/realtime-runtime
13
+ */
14
+ import type { RecordingShard } from './RecordingShardWindow.js';
15
+ /** Resolves true when the server stored the shard; false or a throw are both failures. */
16
+ export type ShardUpload = (shard: RecordingShard) => Promise<boolean>;
17
+ /**
18
+ * Told about shards the queue stops retrying while the recording is live (attempts used up, or the
19
+ * retry list overflowed). They are still in the store; the owner re-sends them once uploads work.
20
+ */
21
+ export type ShardsGivenUp = (shards: RecordingShard[]) => void;
22
+ /** Total upload attempts per shard (the first try plus retries) before it is dropped. */
23
+ export declare const MAX_SHARD_UPLOAD_ATTEMPTS = 3;
24
+ /**
25
+ * Flushes allowed in flight at once. Flushes are ~5 s apart, so overlap only happens when an upload
26
+ * is slower than a tick; an unbounded pile-up on a dead network would start a new batch of uploads
27
+ * every tick. A flush that arrives at capacity does not upload, but it does not lose its shards
28
+ * either: they are retained and the next flush that has a free slot sends them, oldest first.
29
+ */
30
+ export declare const MAX_CONCURRENT_FLUSHES = 2;
31
+ /**
32
+ * Per-attempt upload deadline. A hung request would otherwise pin one of the
33
+ * {@link MAX_CONCURRENT_FLUSHES} slots forever and silently stop all later shards. A timeout counts
34
+ * as a failed attempt; if the request later succeeds anyway that is harmless, because the server
35
+ * key is per-index and a resend of the same bytes is an idempotent overwrite. The timeout is
36
+ * logical only: the transport may accept no AbortSignal, so a timed-out request may still be on the
37
+ * wire. Requests stay rate-bounded regardless (at most {@link MAX_CONCURRENT_FLUSHES} flushes, each
38
+ * sending at most one batch, per timeout window).
39
+ */
40
+ export declare const SHARD_UPLOAD_TIMEOUT_MS = 60000;
41
+ /**
42
+ * Cap on shards held for retry: 10 minutes of 5 s shards. A dead network with flushes at capacity
43
+ * would otherwise grow the retained list (and its bytes) without bound. Overflow drops the oldest;
44
+ * dropped shards remain in the IndexedDB store for the next-load drain.
45
+ */
46
+ export declare const MAX_RETAINED_SHARDS = 120;
47
+ /**
48
+ * Uploads indexed shards and retains failures for retry on later flushes. A retry keeps its shard's
49
+ * index and bytes. Safe to call {@link Flush} from an overlapping timer.
50
+ */
51
+ export declare class RecordingShardQueue {
52
+ private readonly upload;
53
+ private readonly onGivenUp;
54
+ private retained;
55
+ /** Every shard handed to the queue and not yet acknowledged or given up on, by index. */
56
+ private readonly outstanding;
57
+ private inFlight;
58
+ /** True once a capacity stall has been logged; cleared when a flush actually runs. */
59
+ private capacityWarned;
60
+ /** Set by {@link Close} when the recording ends; nothing is sent or retained after it. */
61
+ private closed;
62
+ /**
63
+ * @param upload sends one shard
64
+ * @param onGivenUp told about every shard dropped while the recording is live (not on {@link Close})
65
+ */
66
+ constructor(upload: ShardUpload, onGivenUp?: ShardsGivenUp);
67
+ /** Shards awaiting a retry (for tests and diagnostics). */
68
+ get RetainedCount(): number;
69
+ /**
70
+ * Every shard the queue still holds without an acknowledgement — on the wire, waiting in a flush's
71
+ * batch, or retained for a retry — oldest first. A page that is about to unload re-sends these with
72
+ * `keepalive`, because its ordinary requests are aborted with it.
73
+ */
74
+ Unacknowledged(): RecordingShard[];
75
+ /**
76
+ * Uploads retained shards oldest-first, then `fresh`. A failure never stops the rest of the
77
+ * flush. At {@link MAX_CONCURRENT_FLUSHES} in flight nothing is uploaded and `fresh` is retained
78
+ * for the next flush instead. Never rejects.
79
+ */
80
+ Flush(fresh: RecordingShard[]): Promise<void>;
81
+ /**
82
+ * Ends the queue with its recording: discards retained shards and stops any flush in flight from
83
+ * sending more. An upload already on the wire cannot be aborted, so at most that one request can
84
+ * still land. Discarded shards remain in the IndexedDB store and are drained on a later load.
85
+ */
86
+ Close(): void;
87
+ /** Sorted re-insert: concurrent flushes can fail out of index order, and the oldest must go first. */
88
+ private retain;
89
+ /** Warns once per stall so a wedged uploader is visible without logging every tick. */
90
+ private warnCapacitySkip;
91
+ private uploadWithTimeout;
92
+ private send;
93
+ }
94
+ //# sourceMappingURL=RecordingShardQueue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordingShardQueue.d.ts","sourceRoot":"","sources":["../../src/session/RecordingShardQueue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,0FAA0F;AAC1F,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtE;;;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;CAoCnB"}
@@ -0,0 +1,176 @@
1
+ /** Total upload attempts per shard (the first try plus retries) before it is dropped. */
2
+ export const MAX_SHARD_UPLOAD_ATTEMPTS = 3;
3
+ /**
4
+ * Flushes allowed in flight at once. Flushes are ~5 s apart, so overlap only happens when an upload
5
+ * is slower than a tick; an unbounded pile-up on a dead network would start a new batch of uploads
6
+ * every tick. A flush that arrives at capacity does not upload, but it does not lose its shards
7
+ * either: they are retained and the next flush that has a free slot sends them, oldest first.
8
+ */
9
+ export const MAX_CONCURRENT_FLUSHES = 2;
10
+ /**
11
+ * Per-attempt upload deadline. A hung request would otherwise pin one of the
12
+ * {@link MAX_CONCURRENT_FLUSHES} slots forever and silently stop all later shards. A timeout counts
13
+ * as a failed attempt; if the request later succeeds anyway that is harmless, because the server
14
+ * key is per-index and a resend of the same bytes is an idempotent overwrite. The timeout is
15
+ * logical only: the transport may accept no AbortSignal, so a timed-out request may still be on the
16
+ * wire. Requests stay rate-bounded regardless (at most {@link MAX_CONCURRENT_FLUSHES} flushes, each
17
+ * sending at most one batch, per timeout window).
18
+ */
19
+ export const SHARD_UPLOAD_TIMEOUT_MS = 60_000;
20
+ /**
21
+ * Cap on shards held for retry: 10 minutes of 5 s shards. A dead network with flushes at capacity
22
+ * would otherwise grow the retained list (and its bytes) without bound. Overflow drops the oldest;
23
+ * dropped shards remain in the IndexedDB store for the next-load drain.
24
+ */
25
+ export const MAX_RETAINED_SHARDS = 120;
26
+ /** One-line reason for a warn message; the raw value is still passed along for its stack. */
27
+ function describeFailure(failure) {
28
+ return failure instanceof Error ? failure.message : String(failure);
29
+ }
30
+ /**
31
+ * Uploads indexed shards and retains failures for retry on later flushes. A retry keeps its shard's
32
+ * index and bytes. Safe to call {@link Flush} from an overlapping timer.
33
+ */
34
+ export class RecordingShardQueue {
35
+ /**
36
+ * @param upload sends one shard
37
+ * @param onGivenUp told about every shard dropped while the recording is live (not on {@link Close})
38
+ */
39
+ constructor(upload, onGivenUp = () => undefined) {
40
+ this.upload = upload;
41
+ this.onGivenUp = onGivenUp;
42
+ this.retained = [];
43
+ /** Every shard handed to the queue and not yet acknowledged or given up on, by index. */
44
+ this.outstanding = new Map();
45
+ this.inFlight = 0;
46
+ /** True once a capacity stall has been logged; cleared when a flush actually runs. */
47
+ this.capacityWarned = false;
48
+ /** Set by {@link Close} when the recording ends; nothing is sent or retained after it. */
49
+ this.closed = false;
50
+ }
51
+ /** Shards awaiting a retry (for tests and diagnostics). */
52
+ get RetainedCount() {
53
+ return this.retained.length;
54
+ }
55
+ /**
56
+ * Every shard the queue still holds without an acknowledgement — on the wire, waiting in a flush's
57
+ * batch, or retained for a retry — oldest first. A page that is about to unload re-sends these with
58
+ * `keepalive`, because its ordinary requests are aborted with it.
59
+ */
60
+ Unacknowledged() {
61
+ return [...this.outstanding.values()].sort((a, b) => a.Index - b.Index);
62
+ }
63
+ /**
64
+ * Uploads retained shards oldest-first, then `fresh`. A failure never stops the rest of the
65
+ * flush. At {@link MAX_CONCURRENT_FLUSHES} in flight nothing is uploaded and `fresh` is retained
66
+ * for the next flush instead. Never rejects.
67
+ */
68
+ async Flush(fresh) {
69
+ if (this.closed) {
70
+ if (fresh.length > 0) {
71
+ 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.`);
72
+ }
73
+ return;
74
+ }
75
+ const freshPending = fresh.map((shard) => ({ Shard: shard, Attempts: 0 }));
76
+ for (const shard of fresh) {
77
+ this.outstanding.set(shard.Index, shard);
78
+ }
79
+ if (this.inFlight >= MAX_CONCURRENT_FLUSHES) {
80
+ this.retain(freshPending);
81
+ this.warnCapacitySkip();
82
+ return;
83
+ }
84
+ this.capacityWarned = false;
85
+ this.inFlight++;
86
+ try {
87
+ // Swap the array out so a concurrent flush only sees shards nobody is currently sending.
88
+ const batch = [...this.retained, ...freshPending].sort((a, b) => a.Shard.Index - b.Shard.Index);
89
+ this.retained = [];
90
+ for (const pending of batch) {
91
+ // Re-checked before every send: a flush running when the recording ends must not keep
92
+ // uploading shards the terminal request is about to supersede.
93
+ if (this.closed) {
94
+ return;
95
+ }
96
+ await this.send(pending);
97
+ }
98
+ }
99
+ finally {
100
+ this.inFlight--;
101
+ }
102
+ }
103
+ /**
104
+ * Ends the queue with its recording: discards retained shards and stops any flush in flight from
105
+ * sending more. An upload already on the wire cannot be aborted, so at most that one request can
106
+ * still land. Discarded shards remain in the IndexedDB store and are drained on a later load.
107
+ */
108
+ Close() {
109
+ this.closed = true;
110
+ this.retained = [];
111
+ this.outstanding.clear();
112
+ }
113
+ /** Sorted re-insert: concurrent flushes can fail out of index order, and the oldest must go first. */
114
+ retain(pending) {
115
+ this.retained.push(...pending);
116
+ this.retained.sort((a, b) => a.Shard.Index - b.Shard.Index);
117
+ const overflow = this.retained.length - MAX_RETAINED_SHARDS;
118
+ if (overflow > 0) {
119
+ const dropped = this.retained.splice(0, overflow);
120
+ for (const pending of dropped) {
121
+ this.outstanding.delete(pending.Shard.Index);
122
+ }
123
+ 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.`);
124
+ this.onGivenUp(dropped.map((p) => p.Shard));
125
+ }
126
+ }
127
+ /** Warns once per stall so a wedged uploader is visible without logging every tick. */
128
+ warnCapacitySkip() {
129
+ if (this.capacityWarned) {
130
+ return;
131
+ }
132
+ this.capacityWarned = true;
133
+ 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.`);
134
+ }
135
+ async uploadWithTimeout(shard) {
136
+ let timer;
137
+ const deadline = new Promise((_, reject) => {
138
+ timer = setTimeout(() => reject(new Error(`upload timed out after ${SHARD_UPLOAD_TIMEOUT_MS}ms`)), SHARD_UPLOAD_TIMEOUT_MS);
139
+ });
140
+ try {
141
+ return await Promise.race([this.upload(shard), deadline]);
142
+ }
143
+ finally {
144
+ clearTimeout(timer);
145
+ }
146
+ }
147
+ async send(pending) {
148
+ const { Shard: shard } = pending;
149
+ pending.Attempts++;
150
+ let stored = false;
151
+ let failure = 'server refused the shard';
152
+ try {
153
+ stored = await this.uploadWithTimeout(shard);
154
+ }
155
+ catch (error) {
156
+ failure = error;
157
+ }
158
+ if (stored) {
159
+ this.outstanding.delete(shard.Index);
160
+ return;
161
+ }
162
+ if (this.closed) {
163
+ 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);
164
+ return;
165
+ }
166
+ if (pending.Attempts >= MAX_SHARD_UPLOAD_ATTEMPTS) {
167
+ 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);
168
+ this.outstanding.delete(shard.Index);
169
+ this.onGivenUp([shard]);
170
+ return;
171
+ }
172
+ console.warn(`[RealtimeSession] Recording shard ${shard.Index} upload failed (attempt ${pending.Attempts}/${MAX_SHARD_UPLOAD_ATTEMPTS}: ${describeFailure(failure)}); will retry next flush:`, failure);
173
+ this.retain([pending]);
174
+ }
175
+ }
176
+ //# sourceMappingURL=RecordingShardQueue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordingShardQueue.js","sourceRoot":"","sources":["../../src/session/RecordingShardQueue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @fileoverview Durable queue for realtime-call recording shards, backed by IndexedDB.
3
+ *
4
+ * A shard is written here BEFORE its upload starts and deleted only once the server acknowledges
5
+ * it, so a crash, a closed tab or a dead network never loses audio that was already captured: the
6
+ * next page load drains whatever is left. Persistence is best-effort by design — every failure is
7
+ * logged with the session and shard index and then swallowed into a resolved promise, because a
8
+ * broken disk must never interrupt a live call.
9
+ *
10
+ * @module @memberjunction/realtime-runtime
11
+ */
12
+ /** A shard waiting in the store for the server to acknowledge it. */
13
+ export interface PendingRecordingShard {
14
+ SessionID: string;
15
+ /**
16
+ * The MJ user whose call this is, or null when the page had no signed-in user. A page only lists
17
+ * (and so re-sends) its own user's shards, and logout deletes them: on a shared browser one user's
18
+ * audio is never sent with, or left behind for, another user's session.
19
+ */
20
+ UserID: string | null;
21
+ Index: number;
22
+ Bytes: Uint8Array;
23
+ /** Epoch ms at which the shard was stored; drives oldest-first draining and retention. */
24
+ SavedAt: number;
25
+ }
26
+ /**
27
+ * Durable shard queue. No method throws: a storage failure is logged and the call resolves (empty
28
+ * for {@link IRecordingShardStore.ListOldest}).
29
+ */
30
+ export interface IRecordingShardStore {
31
+ /** Persists a shard (idempotent per session+index). */
32
+ Put(shard: PendingRecordingShard): Promise<void>;
33
+ /** Removes a shard once the server has acknowledged it. */
34
+ Delete(sessionID: string, index: number): Promise<void>;
35
+ /** Purges entries past retention (every user's), then returns up to `limit` of `userID`'s shards, oldest first. */
36
+ ListOldest(limit: number, userID: string | null): Promise<PendingRecordingShard[]>;
37
+ /** Removes every shard of `userID` (logout). */
38
+ DeleteForUser(userID: string | null): Promise<void>;
39
+ }
40
+ /** How long an unacknowledged shard is kept; the server stops accepting late shards well before this. */
41
+ export declare const RECORDING_SHARD_RETENTION_MS: number;
42
+ /** Store used when the host has no IndexedDB (server-side render, some React Native runtimes). */
43
+ export declare class NullRecordingShardStore implements IRecordingShardStore {
44
+ Put(_shard: PendingRecordingShard): Promise<void>;
45
+ Delete(_sessionID: string, _index: number): Promise<void>;
46
+ ListOldest(_limit: number, _userID: string | null): Promise<PendingRecordingShard[]>;
47
+ DeleteForUser(_userID: string | null): Promise<void>;
48
+ }
49
+ /**
50
+ * Creates the shard store for a host. A host without IndexedDB passes `null`/`undefined` and gets a
51
+ * no-op store, so callers never branch on availability.
52
+ *
53
+ * @param factory The host's `indexedDB`, or null/undefined when it has none.
54
+ * @param now Injectable clock for the retention purge; defaults to `Date.now`.
55
+ */
56
+ export declare function CreateRecordingShardStore(factory: IDBFactory | null | undefined, now?: () => number): IRecordingShardStore;
57
+ //# sourceMappingURL=RecordingShardStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordingShardStore.d.ts","sourceRoot":"","sources":["../../src/session/RecordingShardStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;IAClB,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACjC,uDAAuD;IACvD,GAAG,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,2DAA2D;IAC3D,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,mHAAmH;IACnH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;IACnF,gDAAgD;IAChD,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAED,yGAAyG;AACzG,eAAO,MAAM,4BAA4B,QAAoB,CAAC;AAmB9D,kGAAkG;AAClG,qBAAa,uBAAwB,YAAW,oBAAoB;IACnD,GAAG,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAGjD,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAGzD,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAGpF,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAGpE;AAsJD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACrC,OAAO,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,EACtC,GAAG,GAAE,MAAM,MAAiB,GAC7B,oBAAoB,CAEtB"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @fileoverview Durable queue for realtime-call recording shards, backed by IndexedDB.
3
+ *
4
+ * A shard is written here BEFORE its upload starts and deleted only once the server acknowledges
5
+ * it, so a crash, a closed tab or a dead network never loses audio that was already captured: the
6
+ * next page load drains whatever is left. Persistence is best-effort by design — every failure is
7
+ * logged with the session and shard index and then swallowed into a resolved promise, because a
8
+ * broken disk must never interrupt a live call.
9
+ *
10
+ * @module @memberjunction/realtime-runtime
11
+ */
12
+ /** How long an unacknowledged shard is kept; the server stops accepting late shards well before this. */
13
+ export const RECORDING_SHARD_RETENTION_MS = 7 * 24 * 3600_000;
14
+ const DB_NAME = 'mj-realtime-recording';
15
+ const DB_VERSION = 1;
16
+ const STORE_NAME = 'pending-shards';
17
+ const SAVED_AT_INDEX = 'by-saved-at';
18
+ /** Initial open plus one retry; a persistently failing open then degrades to no-ops. */
19
+ const MAX_OPEN_ATTEMPTS = 2;
20
+ /** Store used when the host has no IndexedDB (server-side render, some React Native runtimes). */
21
+ export class NullRecordingShardStore {
22
+ async Put(_shard) {
23
+ // Intentionally empty: without IndexedDB the in-memory retry queue is the only buffer.
24
+ }
25
+ async Delete(_sessionID, _index) {
26
+ // Intentionally empty.
27
+ }
28
+ async ListOldest(_limit, _userID) {
29
+ return [];
30
+ }
31
+ async DeleteForUser(_userID) {
32
+ // Intentionally empty.
33
+ }
34
+ }
35
+ function describeFailure(failure) {
36
+ return failure instanceof Error ? failure.message : String(failure);
37
+ }
38
+ function toTransactionPromise(tx) {
39
+ return new Promise((resolve, reject) => {
40
+ tx.oncomplete = () => resolve();
41
+ tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed'));
42
+ tx.onabort = () => reject(tx.error ?? new Error('IndexedDB transaction aborted'));
43
+ });
44
+ }
45
+ class IndexedDBRecordingShardStore {
46
+ constructor(factory, now) {
47
+ this.factory = factory;
48
+ this.now = now;
49
+ this.db = null;
50
+ this.opening = null;
51
+ this.openAttempts = 0;
52
+ this.disabled = false;
53
+ }
54
+ async Put(shard) {
55
+ const context = `shard ${shard.SessionID}/${shard.Index}`;
56
+ const row = {
57
+ SessionID: shard.SessionID,
58
+ UserID: shard.UserID,
59
+ Index: shard.Index,
60
+ Buffer: shard.Bytes.slice().buffer,
61
+ SavedAt: shard.SavedAt,
62
+ };
63
+ await this.run(context, 'readwrite', (store) => {
64
+ store.put(row);
65
+ });
66
+ }
67
+ async Delete(sessionID, index) {
68
+ await this.run(`shard ${sessionID}/${index}`, 'readwrite', (store) => {
69
+ store.delete([sessionID, index]);
70
+ });
71
+ }
72
+ async ListOldest(limit, userID) {
73
+ const rows = [];
74
+ const cutoff = this.now() - RECORDING_SHARD_RETENTION_MS;
75
+ await this.run('listing pending shards', 'readwrite', (store) => {
76
+ // One ascending walk does both jobs: rows are ordered by age, so every expired row
77
+ // precedes every live one. Purge and read share a transaction so a shard cannot expire
78
+ // between the two steps. (Avoids IDBKeyRange, which is not a global in every host.)
79
+ store.index(SAVED_AT_INDEX).openCursor().onsuccess = (event) => {
80
+ const cursor = event.target.result;
81
+ if (!cursor)
82
+ return;
83
+ const row = cursor.value;
84
+ if (row.SavedAt < cutoff) {
85
+ cursor.delete();
86
+ }
87
+ else if ((row.UserID ?? null) !== userID) {
88
+ // another user's shard: neither listed nor counted against this page's limit
89
+ }
90
+ else if (rows.length < limit) {
91
+ rows.push(row);
92
+ }
93
+ else {
94
+ return;
95
+ }
96
+ cursor.continue();
97
+ };
98
+ });
99
+ return rows.map((r) => ({
100
+ SessionID: r.SessionID,
101
+ UserID: r.UserID ?? null,
102
+ Index: r.Index,
103
+ Bytes: new Uint8Array(r.Buffer),
104
+ SavedAt: r.SavedAt,
105
+ }));
106
+ }
107
+ async DeleteForUser(userID) {
108
+ await this.run(`deleting the stored shards of user ${userID ?? '(none)'}`, 'readwrite', (store) => {
109
+ store.openCursor().onsuccess = (event) => {
110
+ const cursor = event.target.result;
111
+ if (!cursor)
112
+ return;
113
+ if ((cursor.value.UserID ?? null) === userID) {
114
+ cursor.delete();
115
+ }
116
+ cursor.continue();
117
+ };
118
+ });
119
+ }
120
+ /** Runs one transaction; any failure is logged with `context` and swallowed. */
121
+ async run(context, mode, work) {
122
+ const db = await this.getDatabase(context);
123
+ if (!db)
124
+ return;
125
+ try {
126
+ const tx = db.transaction(STORE_NAME, mode);
127
+ const done = toTransactionPromise(tx);
128
+ work(tx.objectStore(STORE_NAME));
129
+ await done;
130
+ }
131
+ catch (failure) {
132
+ console.warn(`[RealtimeSession] Recording shard store failed (${context}): ${describeFailure(failure)}`, failure);
133
+ }
134
+ }
135
+ async getDatabase(context) {
136
+ if (this.db)
137
+ return this.db;
138
+ if (this.disabled)
139
+ return null;
140
+ // Concurrent callers share the in-flight open instead of each counting as an attempt.
141
+ this.opening ??= this.open(context).finally(() => {
142
+ this.opening = null;
143
+ });
144
+ return this.opening;
145
+ }
146
+ async open(context) {
147
+ this.openAttempts++;
148
+ try {
149
+ this.db = await this.openDatabase();
150
+ return this.db;
151
+ }
152
+ catch (failure) {
153
+ console.warn(`[RealtimeSession] Recording shard store could not open IndexedDB (${context}, attempt ${this.openAttempts}/${MAX_OPEN_ATTEMPTS}): ${describeFailure(failure)}`, failure);
154
+ if (this.openAttempts >= MAX_OPEN_ATTEMPTS) {
155
+ this.disabled = true;
156
+ console.warn('[RealtimeSession] Recording shard persistence disabled for this page; shards stay in memory only.');
157
+ }
158
+ return null;
159
+ }
160
+ }
161
+ openDatabase() {
162
+ return new Promise((resolve, reject) => {
163
+ const request = this.factory.open(DB_NAME, DB_VERSION);
164
+ request.onupgradeneeded = () => {
165
+ const store = request.result.createObjectStore(STORE_NAME, { keyPath: ['SessionID', 'Index'] });
166
+ store.createIndex(SAVED_AT_INDEX, 'SavedAt');
167
+ };
168
+ request.onsuccess = () => resolve(request.result);
169
+ request.onerror = () => reject(request.error ?? new Error('IndexedDB open failed'));
170
+ request.onblocked = () => reject(new Error('IndexedDB open blocked'));
171
+ });
172
+ }
173
+ }
174
+ /**
175
+ * Creates the shard store for a host. A host without IndexedDB passes `null`/`undefined` and gets a
176
+ * no-op store, so callers never branch on availability.
177
+ *
178
+ * @param factory The host's `indexedDB`, or null/undefined when it has none.
179
+ * @param now Injectable clock for the retention purge; defaults to `Date.now`.
180
+ */
181
+ export function CreateRecordingShardStore(factory, now = Date.now) {
182
+ return factory ? new IndexedDBRecordingShardStore(factory, now) : new NullRecordingShardStore();
183
+ }
184
+ //# sourceMappingURL=RecordingShardStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordingShardStore.js","sourceRoot":"","sources":["../../src/session/RecordingShardStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /** One sealed, encoded shard ready to store and upload. */
2
+ export interface RecordingShard {
3
+ /** Zero-based, contiguous position in the call's shard sequence. */
4
+ Index: number;
5
+ /** 48 kHz sample offset of the shard's first frame. */
6
+ StartSample: number;
7
+ /** Audio samples the shard covers. */
8
+ SampleCount: number;
9
+ /** The encoded `MJOPUS1` bytes. */
10
+ Bytes: Uint8Array;
11
+ }
12
+ /**
13
+ * Accumulates Opus packets and seals them into shards. Pure: no timers, no I/O.
14
+ *
15
+ * A window seals when it reaches {@link RECORDING_SHARD_MAX_PACKETS} packets (two 5 s windows), or
16
+ * just BEFORE a packet that would push the encoded shard past {@link RECORDING_TERMINAL_MAX_BYTES}.
17
+ * The runtime's 5 s tick seals whatever is pending, so a live call still cuts one ~5 s shard per tick;
18
+ * the packet cap only splits a backlog (a throttled tab's batch). The byte cap guarantees that
19
+ * whatever tail is pending always fits the terminal request body, even after a VBR spike.
20
+ */
21
+ export declare class RecordingShardWindow {
22
+ private pending;
23
+ private pendingBytes;
24
+ private nextIndex;
25
+ private sealedSamples;
26
+ /** Index of the last sealed shard; -1 before any seal. */
27
+ get LastSealedIndex(): number;
28
+ /** End sample of the last sealed shard. */
29
+ get SealedSamples(): number;
30
+ /** End sample of everything appended so far, sealed or pending. */
31
+ get PendingSamples(): number;
32
+ /** Adds packets in capture order and returns every shard that sealed as a result. */
33
+ Append(packets: Uint8Array[]): RecordingShard[];
34
+ /**
35
+ * Seals the partial window now. `sampleCount` (default packets*960) is clamped to
36
+ * [0, packets*960]; the caller passes less when the encoder's last frame was padded. Returns null
37
+ * when no packets are pending, so a second call is a no-op.
38
+ */
39
+ SealTail(sampleCount?: number): RecordingShard | null;
40
+ private seal;
41
+ }
42
+ //# sourceMappingURL=RecordingShardWindow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordingShardWindow.d.ts","sourceRoot":"","sources":["../../src/session/RecordingShardWindow.ts"],"names":[],"mappings":"AAkBA,2DAA2D;AAC3D,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,KAAK,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,aAAa,CAAK;IAE1B,0DAA0D;IAC1D,IAAW,eAAe,IAAI,MAAM,CAEnC;IAED,2CAA2C;IAC3C,IAAW,aAAa,IAAI,MAAM,CAEjC;IAED,mEAAmE;IACnE,IAAW,cAAc,IAAI,MAAM,CAElC;IAED,qFAAqF;IAC9E,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,cAAc,EAAE;IAgBtD;;;;OAIG;IACI,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAS5D,OAAO,CAAC,IAAI;CAmBb"}