@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.
- package/dist/hosts/IRealtimeMediaHost.d.ts +25 -30
- package/dist/hosts/IRealtimeMediaHost.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/session/RealtimeSessionRuntime.d.ts +372 -73
- package/dist/session/RealtimeSessionRuntime.d.ts.map +1 -1
- package/dist/session/RealtimeSessionRuntime.js +919 -212
- package/dist/session/RealtimeSessionRuntime.js.map +1 -1
- package/dist/session/RecordingShardDrain.d.ts +59 -0
- package/dist/session/RecordingShardDrain.d.ts.map +1 -0
- package/dist/session/RecordingShardDrain.js +135 -0
- package/dist/session/RecordingShardDrain.js.map +1 -0
- package/dist/session/RecordingShardQueue.d.ts +64 -38
- package/dist/session/RecordingShardQueue.d.ts.map +1 -1
- package/dist/session/RecordingShardQueue.js +106 -84
- package/dist/session/RecordingShardQueue.js.map +1 -1
- package/dist/session/RecordingShardStore.d.ts +57 -0
- package/dist/session/RecordingShardStore.d.ts.map +1 -0
- package/dist/session/RecordingShardStore.js +184 -0
- package/dist/session/RecordingShardStore.js.map +1 -0
- package/dist/session/RecordingShardWindow.d.ts +42 -0
- package/dist/session/RecordingShardWindow.d.ts.map +1 -0
- package/dist/session/RecordingShardWindow.js +88 -0
- package/dist/session/RecordingShardWindow.js.map +1 -0
- package/dist/session/RecordingTransport.d.ts +67 -0
- package/dist/session/RecordingTransport.d.ts.map +1 -0
- package/dist/session/RecordingTransport.js +61 -0
- package/dist/session/RecordingTransport.js.map +1 -0
- package/package.json +10 -9
|
@@ -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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Cuts a call's continuous Opus packet stream into indexed `MJOPUS1` shards.
|
|
3
|
+
*
|
|
4
|
+
* Shard boundaries are decided here, once, and never revisited: an index is only ever sent with
|
|
5
|
+
* one set of bytes (the server treats a differing resend as a conflict). That is why a tail that is
|
|
6
|
+
* sent early (page hidden) is *sealed* — later packets start a new shard at the next index.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/realtime-runtime
|
|
9
|
+
*/
|
|
10
|
+
import { EncodeRecordingShard, RECORDING_FRAME_SAMPLES, RECORDING_PACKET_LENGTH_PREFIX_BYTES, RECORDING_SHARD_HEADER_BYTES, RECORDING_SHARD_MAX_PACKETS, RECORDING_TERMINAL_MAX_BYTES, } from '@memberjunction/ai-core-plus';
|
|
11
|
+
/**
|
|
12
|
+
* Accumulates Opus packets and seals them into shards. Pure: no timers, no I/O.
|
|
13
|
+
*
|
|
14
|
+
* A window seals when it reaches {@link RECORDING_SHARD_MAX_PACKETS} packets (two 5 s windows), or
|
|
15
|
+
* just BEFORE a packet that would push the encoded shard past {@link RECORDING_TERMINAL_MAX_BYTES}.
|
|
16
|
+
* The runtime's 5 s tick seals whatever is pending, so a live call still cuts one ~5 s shard per tick;
|
|
17
|
+
* the packet cap only splits a backlog (a throttled tab's batch). The byte cap guarantees that
|
|
18
|
+
* whatever tail is pending always fits the terminal request body, even after a VBR spike.
|
|
19
|
+
*/
|
|
20
|
+
export class RecordingShardWindow {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.pending = [];
|
|
23
|
+
this.pendingBytes = RECORDING_SHARD_HEADER_BYTES;
|
|
24
|
+
this.nextIndex = 0;
|
|
25
|
+
this.sealedSamples = 0;
|
|
26
|
+
}
|
|
27
|
+
/** Index of the last sealed shard; -1 before any seal. */
|
|
28
|
+
get LastSealedIndex() {
|
|
29
|
+
return this.nextIndex - 1;
|
|
30
|
+
}
|
|
31
|
+
/** End sample of the last sealed shard. */
|
|
32
|
+
get SealedSamples() {
|
|
33
|
+
return this.sealedSamples;
|
|
34
|
+
}
|
|
35
|
+
/** End sample of everything appended so far, sealed or pending. */
|
|
36
|
+
get PendingSamples() {
|
|
37
|
+
return this.sealedSamples + this.pending.length * RECORDING_FRAME_SAMPLES;
|
|
38
|
+
}
|
|
39
|
+
/** Adds packets in capture order and returns every shard that sealed as a result. */
|
|
40
|
+
Append(packets) {
|
|
41
|
+
const sealed = [];
|
|
42
|
+
for (const packet of packets) {
|
|
43
|
+
const cost = RECORDING_PACKET_LENGTH_PREFIX_BYTES + packet.length;
|
|
44
|
+
if (this.pending.length > 0 && this.pendingBytes + cost > RECORDING_TERMINAL_MAX_BYTES) {
|
|
45
|
+
sealed.push(this.seal(this.pending.length * RECORDING_FRAME_SAMPLES));
|
|
46
|
+
}
|
|
47
|
+
this.pending.push(packet);
|
|
48
|
+
this.pendingBytes += cost;
|
|
49
|
+
if (this.pending.length >= RECORDING_SHARD_MAX_PACKETS) {
|
|
50
|
+
sealed.push(this.seal(this.pending.length * RECORDING_FRAME_SAMPLES));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return sealed;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Seals the partial window now. `sampleCount` (default packets*960) is clamped to
|
|
57
|
+
* [0, packets*960]; the caller passes less when the encoder's last frame was padded. Returns null
|
|
58
|
+
* when no packets are pending, so a second call is a no-op.
|
|
59
|
+
*/
|
|
60
|
+
SealTail(sampleCount) {
|
|
61
|
+
if (this.pending.length === 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const max = this.pending.length * RECORDING_FRAME_SAMPLES;
|
|
65
|
+
const wanted = sampleCount ?? max;
|
|
66
|
+
return this.seal(Math.min(Math.max(Math.floor(wanted), 0), max));
|
|
67
|
+
}
|
|
68
|
+
seal(sampleCount) {
|
|
69
|
+
const shard = {
|
|
70
|
+
Index: this.nextIndex,
|
|
71
|
+
StartSample: this.sealedSamples,
|
|
72
|
+
SampleCount: sampleCount,
|
|
73
|
+
Bytes: EncodeRecordingShard({
|
|
74
|
+
ShardIndex: this.nextIndex,
|
|
75
|
+
StartSample: this.sealedSamples,
|
|
76
|
+
SampleCount: sampleCount,
|
|
77
|
+
PacketCount: this.pending.length,
|
|
78
|
+
Packets: this.pending,
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
this.nextIndex++;
|
|
82
|
+
this.sealedSamples += sampleCount;
|
|
83
|
+
this.pending = [];
|
|
84
|
+
this.pendingBytes = RECORDING_SHARD_HEADER_BYTES;
|
|
85
|
+
return shard;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=RecordingShardWindow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RecordingShardWindow.js","sourceRoot":"","sources":["../../src/session/RecordingShardWindow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `fetch` transport for recording shards and the terminal request.
|
|
3
|
+
*
|
|
4
|
+
* Shards travel as raw bytes (no base64, no GraphQL envelope) to MJAPI's REST recording routes. The
|
|
5
|
+
* terminal request sets `keepalive: true` so the browser completes it even while the page is
|
|
6
|
+
* unloading; that flag caps the body at 64 KiB, which is why the window seals tails under 48 KiB.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/realtime-runtime
|
|
9
|
+
*/
|
|
10
|
+
import type { RecordingShard } from './RecordingShardWindow.js';
|
|
11
|
+
import type { PendingRecordingShard } from './RecordingShardStore.js';
|
|
12
|
+
export interface RecordingTerminalInput {
|
|
13
|
+
/** The final partial shard's bytes, or null when every shard was already sent. */
|
|
14
|
+
Tail: Uint8Array | null;
|
|
15
|
+
LastShardIndex: number;
|
|
16
|
+
CapturedSamples: number;
|
|
17
|
+
/**
|
|
18
|
+
* False when the call may go on (the tab was only hidden): the server stamps the counts but assembles
|
|
19
|
+
* after its grace period instead of now. Absent or true: the call ended.
|
|
20
|
+
*/
|
|
21
|
+
Final?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Per-upload options for {@link IRecordingTransport.UploadShard}. */
|
|
24
|
+
export interface RecordingUploadOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Send with `keepalive` so the request survives page unload. Only for shards sealed inside a
|
|
27
|
+
* `pagehide`/`visibilitychange` handler: the browser caps in-flight keepalive bodies (64 KiB per
|
|
28
|
+
* page), and the browser rejects a request over that budget before sending it — which resolves
|
|
29
|
+
* `Retry` like any network failure (not `Refused`: the server never saw it).
|
|
30
|
+
*/
|
|
31
|
+
Keepalive?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* What became of one shard upload. `Acked`: the server has it. `Refused`: the server answered that it
|
|
35
|
+
* will never take this shard (bad shard, not this caller's session, conflicting bytes, too late, too
|
|
36
|
+
* big), so re-sending it is pointless. `Retry`: anything that may succeed later (no network, an expired
|
|
37
|
+
* token, a busy or failing server).
|
|
38
|
+
*/
|
|
39
|
+
export type RecordingUploadOutcome = 'Acked' | 'Refused' | 'Retry';
|
|
40
|
+
/** Sends recording bytes to the server. */
|
|
41
|
+
export interface IRecordingTransport {
|
|
42
|
+
UploadShard(sessionID: string, shard: RecordingShard | PendingRecordingShard, options?: RecordingUploadOptions): Promise<RecordingUploadOutcome>;
|
|
43
|
+
/** Sent with `keepalive` so it survives page unload. Resolves true only on a server ack. */
|
|
44
|
+
SendTerminal(sessionID: string, input: RecordingTerminalInput): Promise<boolean>;
|
|
45
|
+
}
|
|
46
|
+
export interface FetchRecordingTransportConfig {
|
|
47
|
+
/** MJAPI base URL without a trailing slash (see {@link RecordingBaseURLFromGraphQLURL}). */
|
|
48
|
+
BaseURL: string;
|
|
49
|
+
/** Called per request so a refreshed token is picked up. */
|
|
50
|
+
Headers: () => Record<string, string>;
|
|
51
|
+
/** Defaults to `globalThis.fetch`, resolved at call time so tests can inject one. */
|
|
52
|
+
Fetch?: typeof fetch;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Derives the REST base from the GraphQL endpoint URL by dropping trailing slashes and `/graphql`.
|
|
56
|
+
* A scan, not a regex: `/\/+$/` backtracks quadratically on a long run of slashes that does not end
|
|
57
|
+
* the string, and the URL comes from host configuration.
|
|
58
|
+
*/
|
|
59
|
+
export declare function RecordingBaseURLFromGraphQLURL(url: string): string;
|
|
60
|
+
export declare class FetchRecordingTransport implements IRecordingTransport {
|
|
61
|
+
private readonly config;
|
|
62
|
+
constructor(config: FetchRecordingTransportConfig);
|
|
63
|
+
UploadShard(sessionID: string, shard: RecordingShard | PendingRecordingShard, options?: RecordingUploadOptions): Promise<RecordingUploadOutcome>;
|
|
64
|
+
SendTerminal(sessionID: string, input: RecordingTerminalInput): Promise<boolean>;
|
|
65
|
+
private post;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=RecordingTransport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RecordingTransport.d.ts","sourceRoot":"","sources":["../../src/session/RecordingTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEnE,MAAM,WAAW,sBAAsB;IACnC,kFAAkF;IAClF,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,sEAAsE;AACtE,MAAM,WAAW,sBAAsB;IACnC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AASnE,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IAChC,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,qBAAqB,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACjJ,4FAA4F;IAC5F,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,6BAA6B;IAC1C,4FAA4F;IAC5F,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qFAAqF;IACrF,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAQD;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOlE;AAED,qBAAa,uBAAwB,YAAW,mBAAmB;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,6BAA6B;IAE3D,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,qBAAqB,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAK1I,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC;YAO/E,IAAI;CAqBrB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statuses that mean the server will never accept this shard, whoever sends it and however often.
|
|
3
|
+
* 401 is not here (a refreshed token can still send it), nor 404: an MJAPI older than the recording
|
|
4
|
+
* routes answers 404 to every shard, and a browser upgraded first must keep its audio for the server.
|
|
5
|
+
*/
|
|
6
|
+
const PERMANENT_REFUSAL_STATUSES = new Set([400, 403, 409, 410, 413]);
|
|
7
|
+
function describeFailure(failure) {
|
|
8
|
+
return failure instanceof Error ? failure.message : String(failure);
|
|
9
|
+
}
|
|
10
|
+
const GRAPHQL_SUFFIX = '/graphql';
|
|
11
|
+
/**
|
|
12
|
+
* Derives the REST base from the GraphQL endpoint URL by dropping trailing slashes and `/graphql`.
|
|
13
|
+
* A scan, not a regex: `/\/+$/` backtracks quadratically on a long run of slashes that does not end
|
|
14
|
+
* the string, and the URL comes from host configuration.
|
|
15
|
+
*/
|
|
16
|
+
export function RecordingBaseURLFromGraphQLURL(url) {
|
|
17
|
+
let end = url.length;
|
|
18
|
+
while (end > 0 && url.charCodeAt(end - 1) === 47 /* '/' */) {
|
|
19
|
+
end--;
|
|
20
|
+
}
|
|
21
|
+
const trimmed = url.slice(0, end);
|
|
22
|
+
return trimmed.endsWith(GRAPHQL_SUFFIX) ? trimmed.slice(0, -GRAPHQL_SUFFIX.length) : trimmed;
|
|
23
|
+
}
|
|
24
|
+
export class FetchRecordingTransport {
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
UploadShard(sessionID, shard, options) {
|
|
29
|
+
const url = `${this.config.BaseURL}/realtime/recording/${encodeURIComponent(sessionID)}/shard/${shard.Index}`;
|
|
30
|
+
return this.post(url, shard.Bytes, options?.Keepalive === true, `shard ${sessionID}/${shard.Index}`);
|
|
31
|
+
}
|
|
32
|
+
async SendTerminal(sessionID, input) {
|
|
33
|
+
const final = input.Final === false ? '&final=false' : '';
|
|
34
|
+
const query = `lastShardIndex=${input.LastShardIndex}&capturedSamples=${input.CapturedSamples}${final}`;
|
|
35
|
+
const url = `${this.config.BaseURL}/realtime/recording/${encodeURIComponent(sessionID)}/end?${query}`;
|
|
36
|
+
return (await this.post(url, input.Tail ?? new Uint8Array(0), true, `terminal ${sessionID}/${input.LastShardIndex}`)) === 'Acked';
|
|
37
|
+
}
|
|
38
|
+
async post(url, body, keepalive, context) {
|
|
39
|
+
const doFetch = this.config.Fetch ?? globalThis.fetch;
|
|
40
|
+
try {
|
|
41
|
+
const response = await doFetch(url, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/octet-stream', ...this.config.Headers() },
|
|
44
|
+
// TS types a bare Uint8Array as possibly SharedArrayBuffer-backed; shard bytes never are.
|
|
45
|
+
body: body,
|
|
46
|
+
...(keepalive ? { keepalive: true } : {}),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const outcome = PERMANENT_REFUSAL_STATUSES.has(response.status) ? 'Refused' : 'Retry';
|
|
50
|
+
console.warn(`[RealtimeSession] Recording ${context} rejected by server with status ${response.status} (${outcome === 'Refused' ? 'will not be re-sent' : 'will be retried'})`);
|
|
51
|
+
return outcome;
|
|
52
|
+
}
|
|
53
|
+
return 'Acked';
|
|
54
|
+
}
|
|
55
|
+
catch (failure) {
|
|
56
|
+
console.warn(`[RealtimeSession] Recording ${context} request failed: ${describeFailure(failure)}`, failure);
|
|
57
|
+
return 'Retry';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=RecordingTransport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RecordingTransport.js","sourceRoot":"","sources":["../../src/session/RecordingTransport.ts"],"names":[],"mappings":"AA2CA;;;;GAIG;AACH,MAAM,0BAA0B,GAAwB,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAkB3F,SAAS,eAAe,CAAC,OAAgB;IACrC,OAAO,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,cAAc,GAAG,UAAU,CAAC;AAElC;;;;GAIG;AACH,MAAM,UAAU,8BAA8B,CAAC,GAAW;IACtD,IAAI,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC;IACrB,OAAO,GAAG,GAAG,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,CAAC;QACzD,GAAG,EAAE,CAAC;IACV,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAClC,OAAO,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AACjG,CAAC;AAED,MAAM,OAAO,uBAAuB;IAChC,YAA6B,MAAqC;QAArC,WAAM,GAAN,MAAM,CAA+B;IAAG,CAAC;IAE/D,WAAW,CAAC,SAAiB,EAAE,KAA6C,EAAE,OAAgC;QACjH,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,uBAAuB,kBAAkB,CAAC,SAAS,CAAC,UAAU,KAAK,CAAC,KAAK,EAAE,CAAC;QAC9G,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,KAAK,IAAI,EAAE,SAAS,SAAS,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IACzG,CAAC;IAEM,KAAK,CAAC,YAAY,CAAC,SAAiB,EAAE,KAA6B;QACtE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1D,MAAM,KAAK,GAAG,kBAAkB,KAAK,CAAC,cAAc,oBAAoB,KAAK,CAAC,eAAe,GAAG,KAAK,EAAE,CAAC;QACxG,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,uBAAuB,kBAAkB,CAAC,SAAS,CAAC,QAAQ,KAAK,EAAE,CAAC;QACtG,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,SAAS,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC;IACtI,CAAC;IAEO,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,IAAgB,EAAE,SAAkB,EAAE,OAAe;QACjF,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;QACtD,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE;gBAChC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE;gBACjF,0FAA0F;gBAC1F,IAAI,EAAE,IAA+B;gBACrC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5C,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACf,MAAM,OAAO,GAA2B,0BAA0B,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;gBAC9G,OAAO,CAAC,IAAI,CAAC,+BAA+B,OAAO,mCAAmC,QAAQ,CAAC,MAAM,KAAK,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,iBAAiB,GAAG,CAAC,CAAC;gBAChL,OAAO,OAAO,CAAC;YACnB,CAAC;YACD,OAAO,OAAO,CAAC;QACnB,CAAC;QAAC,OAAO,OAAO,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,+BAA+B,OAAO,oBAAoB,eAAe,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;YAC5G,OAAO,OAAO,CAAC;QACnB,CAAC;IACL,CAAC;CACJ"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/realtime-runtime",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "6.2.0-edge.
|
|
4
|
+
"version": "6.2.0-edge.6",
|
|
5
5
|
"description": "MemberJunction: Framework-agnostic Realtime Session Runtime — client-direct realtime co-agent session orchestration (mint, driver resolution, transcripts, tool relay, delegation narration, channel lifecycle, usage relay). Pure TypeScript, zero UX dependencies. Consumable from browser (Angular/React/Vue), React Native, or Node test harnesses.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -11,18 +11,19 @@
|
|
|
11
11
|
"author": "MemberJunction.com",
|
|
12
12
|
"license": "BUSL-1.1",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@memberjunction/ai": "6.2.0-edge.
|
|
15
|
-
"@memberjunction/ai-core-plus": "6.2.0-edge.
|
|
16
|
-
"@memberjunction/ai-engine-base": "6.2.0-edge.
|
|
17
|
-
"@memberjunction/ai-realtime-client": "6.2.0-edge.
|
|
18
|
-
"@memberjunction/core": "6.2.0-edge.
|
|
19
|
-
"@memberjunction/core-entities": "6.2.0-edge.
|
|
20
|
-
"@memberjunction/global": "6.2.0-edge.
|
|
21
|
-
"@memberjunction/graphql-dataprovider": "6.2.0-edge.
|
|
14
|
+
"@memberjunction/ai": "6.2.0-edge.6",
|
|
15
|
+
"@memberjunction/ai-core-plus": "6.2.0-edge.6",
|
|
16
|
+
"@memberjunction/ai-engine-base": "6.2.0-edge.6",
|
|
17
|
+
"@memberjunction/ai-realtime-client": "6.2.0-edge.6",
|
|
18
|
+
"@memberjunction/core": "6.2.0-edge.6",
|
|
19
|
+
"@memberjunction/core-entities": "6.2.0-edge.6",
|
|
20
|
+
"@memberjunction/global": "6.2.0-edge.6",
|
|
21
|
+
"@memberjunction/graphql-dataprovider": "6.2.0-edge.6",
|
|
22
22
|
"rxjs": "^7.8.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "24.10.11",
|
|
26
|
+
"fake-indexeddb": "^6.2.5",
|
|
26
27
|
"typescript": "^5.9.3"
|
|
27
28
|
},
|
|
28
29
|
"repository": {
|