agl 40.6.0 → 41.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +33 -0
- package/dist_serve/bundle.js +3514 -3357
- package/dist_serve/bundle.js.map +2 -2
- package/dist_serve/bundle.js.third-party-notices.json +58 -32
- package/dist_serve/bundle.js.third-party-notices.txt +108 -33
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +64 -37
- package/dist_ts/classes.controller.d.ts +8 -0
- package/dist_ts/classes.controller.js +109 -21
- package/dist_ts/classes.controllereventdelivery.d.ts +72 -0
- package/dist_ts/classes.controllereventdelivery.js +233 -0
- package/dist_ts/classes.flexmodels.public.js +4 -1
- package/dist_ts/classes.flexprojectionreader.d.ts +6 -0
- package/dist_ts/classes.flexprojectionreader.js +29 -5
- package/dist_ts/classes.flexservice.d.ts +2 -1
- package/dist_ts/classes.flexservice.js +34 -6
- package/dist_ts/classes.flexstore.d.ts +41 -4
- package/dist_ts/classes.flexstore.js +266 -203
- package/dist_ts/classes.ticketnotifier.js +4 -2
- package/dist_ts/classes.ticketstore.d.ts +50 -2
- package/dist_ts/classes.ticketstore.js +195 -78
- package/dist_ts/driver/classes.drivereventrelay.d.ts +13 -1
- package/dist_ts/driver/classes.drivereventrelay.js +99 -6
- package/dist_ts/driver/functions.driverinstructions.d.ts +1 -1
- package/dist_ts/driver/functions.driverinstructions.js +1 -1
- package/dist_ts/driver/functions.drivertools.d.ts +11 -2
- package/dist_ts/driver/functions.drivertools.js +19 -9
- package/dist_ts/fleet/classes.boardpump.d.ts +8 -2
- package/dist_ts/fleet/classes.boardpump.js +38 -20
- package/dist_ts/fleet/classes.ticketeffects.d.ts +17 -2
- package/dist_ts/fleet/classes.ticketeffects.js +16 -10
- package/dist_ts/fleet/classes.workers.d.ts +4 -0
- package/dist_ts/fleet/classes.workers.js +37 -17
- package/dist_ts/fleet/functions.workervalidation.js +2 -2
- package/dist_ts/functions.flexprivatesnapshot.d.ts +102 -0
- package/dist_ts/functions.flexprivatesnapshot.js +461 -0
- package/dist_ts/functions.mcptickettools.d.ts +3 -0
- package/dist_ts/functions.mcptickettools.js +2 -2
- package/dist_ts/functions.ticketcli.d.ts +1 -1
- package/dist_ts/functions.ticketcli.js +8 -4
- package/dist_ts/functions.ticketvalidation.d.ts +3 -0
- package/dist_ts/functions.ticketvalidation.js +7 -4
- package/dist_ts/interfaces.flexipc.d.ts +21 -2
- package/dist_ts/interfaces.flexipc.js +33 -16
- package/dist_ts_interfaces/fleetworkers.d.ts +6 -0
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/requests.tickets.d.ts +8 -12
- package/dist_ts_interfaces/tickets.d.ts +37 -9
- package/dist_ts_interfaces/tickets.js +7 -1
- package/package.json +7 -7
- package/readme.md +54 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +63 -36
- package/ts/classes.controller.ts +131 -31
- package/ts/classes.controllereventdelivery.ts +289 -0
- package/ts/classes.flexmodels.public.ts +10 -0
- package/ts/classes.flexprojectionreader.ts +30 -3
- package/ts/classes.flexservice.ts +33 -5
- package/ts/classes.flexstore.ts +381 -246
- package/ts/classes.ticketnotifier.ts +3 -1
- package/ts/classes.ticketstore.ts +201 -75
- package/ts/driver/classes.drivereventrelay.ts +89 -4
- package/ts/driver/functions.driverinstructions.ts +1 -1
- package/ts/driver/functions.drivertools.ts +22 -8
- package/ts/fleet/classes.boardpump.ts +51 -19
- package/ts/fleet/classes.ticketeffects.ts +25 -11
- package/ts/fleet/classes.workers.ts +28 -16
- package/ts/fleet/functions.workervalidation.ts +1 -1
- package/ts/functions.flexprivatesnapshot.ts +601 -0
- package/ts/functions.mcptickettools.ts +1 -1
- package/ts/functions.ticketcli.ts +7 -3
- package/ts/functions.ticketvalidation.ts +7 -3
- package/ts/interfaces.flexipc.ts +50 -21
- package/ts_interfaces/fleetworkers.ts +6 -0
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/requests.tickets.ts +9 -6
- package/ts_interfaces/tickets.ts +32 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements.harnesscontrollerapp.ts +19 -8
- package/ts_web/elements.ticketboard.ts +16 -4
- package/ts_web/settings.general.ts +10 -4
package/ts/classes.controller.ts
CHANGED
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
} from './functions.canonicaldirectory.js';
|
|
88
88
|
import { commitinfo } from './00_commitinfo_data.js';
|
|
89
89
|
import { controllerFailureJournal } from './classes.failurejournal.js';
|
|
90
|
+
import { ControllerEventDelivery } from './classes.controllereventdelivery.js';
|
|
90
91
|
import {
|
|
91
92
|
controllerPackageName,
|
|
92
93
|
controllerBuiltinCommands,
|
|
@@ -929,6 +930,9 @@ const maxSessionArchiveReconciliationReads = 3;
|
|
|
929
930
|
const maxLatestOpenCodeToolExecutions = 512;
|
|
930
931
|
const maxLatestOpenCodeMessageUpdates = 512;
|
|
931
932
|
const maxPendingFlexControllerEvents = 512;
|
|
933
|
+
/** Events and serialized bytes one browser may have waiting before its queue collapses into a resync. */
|
|
934
|
+
const maxPendingControllerEventsPerViewer = 512;
|
|
935
|
+
const maxPendingControllerEventBytesPerViewer = 16 * 1024 * 1024;
|
|
932
936
|
const maxLatestFlexToolExecutions = 512;
|
|
933
937
|
const maxLatestFlexToolExecutionBytes = 8 * 1024 * 1024;
|
|
934
938
|
const maxLatestFlexMessageUpdateBytes = 16 * 1024 * 1024;
|
|
@@ -1352,6 +1356,12 @@ interface ILatestTextUpdate {
|
|
|
1352
1356
|
bytes?: number;
|
|
1353
1357
|
}
|
|
1354
1358
|
|
|
1359
|
+
/** A live Flex reasoning snapshot with the accounting its deltas extend. */
|
|
1360
|
+
type TLatestFlexReasoningUpdate = ILatestReasoningUpdate & IFlexLiveTextAccounting;
|
|
1361
|
+
|
|
1362
|
+
/** A live Flex text snapshot with the accounting its deltas extend. */
|
|
1363
|
+
type TLatestFlexTextUpdate = ILatestTextUpdate & IFlexLiveTextAccounting;
|
|
1364
|
+
|
|
1355
1365
|
type TControllerMessageDelta = IControllerReasoningDelta | IControllerTextDelta;
|
|
1356
1366
|
|
|
1357
1367
|
interface IControllerTextDeltaFragment {
|
|
@@ -1389,6 +1399,67 @@ const controllerTextAfterAppendUtf8Bytes = (
|
|
|
1389
1399
|
return Number.isSafeInteger(result) && result >= 0 ? result : undefined;
|
|
1390
1400
|
};
|
|
1391
1401
|
|
|
1402
|
+
const isHighSurrogate = (codeArg: number): boolean => codeArg >= 0xD800 && codeArg <= 0xDBFF;
|
|
1403
|
+
const isLowSurrogate = (codeArg: number): boolean => codeArg >= 0xDC00 && codeArg <= 0xDFFF;
|
|
1404
|
+
|
|
1405
|
+
/** The UTF-8 bytes `JSON.stringify` writes for a string's content (without its quotes), counted without serializing. */
|
|
1406
|
+
const controllerJsonStringContentBytes = (valueArg: string): number => {
|
|
1407
|
+
let bytes = 0;
|
|
1408
|
+
for (let index = 0; index < valueArg.length; index += 1) {
|
|
1409
|
+
const code = valueArg.charCodeAt(index);
|
|
1410
|
+
if (
|
|
1411
|
+
code === 0x22 || code === 0x5C
|
|
1412
|
+
|| code === 0x08 || code === 0x09 || code === 0x0A || code === 0x0C || code === 0x0D
|
|
1413
|
+
) bytes += 2;
|
|
1414
|
+
else if (code < 0x20) bytes += 6;
|
|
1415
|
+
else if (code < 0x80) bytes += 1;
|
|
1416
|
+
else if (code < 0x800) bytes += 2;
|
|
1417
|
+
else if (isHighSurrogate(code)) {
|
|
1418
|
+
if (index + 1 < valueArg.length && isLowSurrogate(valueArg.charCodeAt(index + 1))) {
|
|
1419
|
+
bytes += 4;
|
|
1420
|
+
index += 1;
|
|
1421
|
+
} else bytes += 6;
|
|
1422
|
+
} else if (isLowSurrogate(code)) bytes += 6;
|
|
1423
|
+
else bytes += 3;
|
|
1424
|
+
}
|
|
1425
|
+
return bytes;
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* What a live Flex snapshot's text costs, kept beside the text so a delta is accounted by its own length: the
|
|
1430
|
+
* cumulative text is never encoded, serialized or even read again on the append path.
|
|
1431
|
+
*/
|
|
1432
|
+
interface IFlexLiveTextAccounting {
|
|
1433
|
+
textUtf8Bytes: number;
|
|
1434
|
+
/** `JSON.stringify(text)` bytes without the quotes. */
|
|
1435
|
+
textJsonBytes: number;
|
|
1436
|
+
textEndsWithHighSurrogate: boolean;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const flexLiveTextAccounting = (textArg: string): IFlexLiveTextAccounting => ({
|
|
1440
|
+
textUtf8Bytes: Buffer.byteLength(textArg, 'utf8'),
|
|
1441
|
+
textJsonBytes: controllerJsonStringContentBytes(textArg),
|
|
1442
|
+
textEndsWithHighSurrogate: textArg.length > 0 && isHighSurrogate(textArg.charCodeAt(textArg.length - 1)),
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
const flexLiveTextAccountingAfterAppend = (
|
|
1446
|
+
accountingArg: IFlexLiveTextAccounting,
|
|
1447
|
+
deltaArg: string,
|
|
1448
|
+
): IFlexLiveTextAccounting | undefined => {
|
|
1449
|
+
if (deltaArg.length === 0) return accountingArg;
|
|
1450
|
+
// A surrogate pair split across the append: two lone halves (3 + 3 UTF-8 bytes, `\uXXXX` + `\uXXXX` in JSON)
|
|
1451
|
+
// become one pair (4 bytes in both).
|
|
1452
|
+
const joinsPair = accountingArg.textEndsWithHighSurrogate && isLowSurrogate(deltaArg.charCodeAt(0));
|
|
1453
|
+
const textUtf8Bytes = accountingArg.textUtf8Bytes + Buffer.byteLength(deltaArg, 'utf8') - (joinsPair ? 2 : 0);
|
|
1454
|
+
const textJsonBytes = accountingArg.textJsonBytes + controllerJsonStringContentBytes(deltaArg) - (joinsPair ? 8 : 0);
|
|
1455
|
+
if (!Number.isSafeInteger(textUtf8Bytes) || !Number.isSafeInteger(textJsonBytes)) return undefined;
|
|
1456
|
+
return {
|
|
1457
|
+
textUtf8Bytes,
|
|
1458
|
+
textJsonBytes,
|
|
1459
|
+
textEndsWithHighSurrogate: isHighSurrogate(deltaArg.charCodeAt(deltaArg.length - 1)),
|
|
1460
|
+
};
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1392
1463
|
const fragmentControllerTextDelta = (valueArg: string): IControllerTextDeltaFragment[] => {
|
|
1393
1464
|
const fragments: IControllerTextDeltaFragment[] = [];
|
|
1394
1465
|
let delta = '';
|
|
@@ -3031,8 +3102,8 @@ export class OpenCodeController {
|
|
|
3031
3102
|
private readonly latestOpenCodeReasoningUpdates = new Map<string, ILatestReasoningUpdate>();
|
|
3032
3103
|
private readonly latestOpenCodeTextUpdates = new Map<string, ILatestTextUpdate>();
|
|
3033
3104
|
private readonly latestFlexToolExecutions = new Map<string, ILatestToolExecution>();
|
|
3034
|
-
private readonly latestFlexReasoningUpdates = new Map<string,
|
|
3035
|
-
private readonly latestFlexTextUpdates = new Map<string,
|
|
3105
|
+
private readonly latestFlexReasoningUpdates = new Map<string, TLatestFlexReasoningUpdate>();
|
|
3106
|
+
private readonly latestFlexTextUpdates = new Map<string, TLatestFlexTextUpdate>();
|
|
3036
3107
|
private readonly pendingFlexControllerEvents = new Map<string, IControllerEvent>();
|
|
3037
3108
|
private readonly pendingFlexAutoAcceptSweeps = new Map<string, IFlexAutoAcceptSweep>();
|
|
3038
3109
|
private openCodeControllerEventDrainTask?: Promise<void>;
|
|
@@ -3049,6 +3120,7 @@ export class OpenCodeController {
|
|
|
3049
3120
|
private pendingFlexToolEventBytes = 0;
|
|
3050
3121
|
private pendingFlexMessageEventBytes = 0;
|
|
3051
3122
|
private flexControllerEventDrainTask?: Promise<void>;
|
|
3123
|
+
private controllerEventDelivery?: ControllerEventDelivery;
|
|
3052
3124
|
private flexAutoAcceptDrainTask?: Promise<void>;
|
|
3053
3125
|
/** Sweeps whose auto-accept failed, queued again when the retry timer fires. */
|
|
3054
3126
|
private readonly failedFlexAutoAcceptSweeps = new Map<string, IFlexAutoAcceptSweep>();
|
|
@@ -13230,6 +13302,7 @@ export class OpenCodeController {
|
|
|
13230
13302
|
...(this.openCodeControllerEventDrainTask ? [this.openCodeControllerEventDrainTask] : []),
|
|
13231
13303
|
...(this.flexControllerEventDrainTask ? [this.flexControllerEventDrainTask] : []),
|
|
13232
13304
|
]);
|
|
13305
|
+
await this.controllerEventDelivery?.stop();
|
|
13233
13306
|
|
|
13234
13307
|
if (this.flexProjectionReader) {
|
|
13235
13308
|
await stopPart(async () => {
|
|
@@ -16757,7 +16830,7 @@ export class OpenCodeController {
|
|
|
16757
16830
|
return { owner, ...(configuration.boardPump ? { configuration: configuration.boardPump } : {}) };
|
|
16758
16831
|
},
|
|
16759
16832
|
tickets: async () => (await this.ticketStore.execute('list', {}, undefined)).tickets,
|
|
16760
|
-
|
|
16833
|
+
history: (ticketKeyArg) => this.ticketStore.history(ticketKeyArg),
|
|
16761
16834
|
workers: (ownerArg) => this.requireFleetWorkers().views(ownerArg),
|
|
16762
16835
|
steer: (sessionIdArg, ticketKeyArg, textArg) => this.requireFleetWorkers().notify(sessionIdArg, ticketKeyArg, textArg),
|
|
16763
16836
|
publish: (eventArg, ownerArg) => this.requireFleetWorkers().events.publish(eventArg, ownerArg),
|
|
@@ -17631,6 +17704,7 @@ export class OpenCodeController {
|
|
|
17631
17704
|
driver: {
|
|
17632
17705
|
instructions: service.instructions(await this.readDriverConfiguration()),
|
|
17633
17706
|
notes: state.scratchpad.text,
|
|
17707
|
+
notesRevision: state.scratchpad.revision,
|
|
17634
17708
|
},
|
|
17635
17709
|
};
|
|
17636
17710
|
}
|
|
@@ -23851,7 +23925,8 @@ export class OpenCodeController {
|
|
|
23851
23925
|
let cursor: string | undefined;
|
|
23852
23926
|
for (let pages = 0; ; pages++) {
|
|
23853
23927
|
if (pages >= 1000) throw new Error('Flex delivery transcript exceeded its inspection limit.');
|
|
23854
|
-
|
|
23928
|
+
// Metadata pages: identity and state only, so a full scan never moves the transcript's text.
|
|
23929
|
+
const page = await supervisorArg.request('message.metadata.page', {
|
|
23855
23930
|
...payload, limit: 50, includeUndone: true, ...(cursor === undefined ? {} : { before: cursor }),
|
|
23856
23931
|
}, signal);
|
|
23857
23932
|
for (const message of page.messages) {
|
|
@@ -29188,7 +29263,7 @@ export class OpenCodeController {
|
|
|
29188
29263
|
|
|
29189
29264
|
private setLatestFlexReasoningUpdate(
|
|
29190
29265
|
keyArg: string,
|
|
29191
|
-
entryArg:
|
|
29266
|
+
entryArg: TLatestFlexReasoningUpdate,
|
|
29192
29267
|
bytesArg: number,
|
|
29193
29268
|
): void {
|
|
29194
29269
|
this.deleteLatestFlexReasoningUpdate(keyArg);
|
|
@@ -29198,7 +29273,7 @@ export class OpenCodeController {
|
|
|
29198
29273
|
|
|
29199
29274
|
private setLatestFlexTextUpdate(
|
|
29200
29275
|
keyArg: string,
|
|
29201
|
-
entryArg:
|
|
29276
|
+
entryArg: TLatestFlexTextUpdate,
|
|
29202
29277
|
bytesArg: number,
|
|
29203
29278
|
): void {
|
|
29204
29279
|
this.deleteLatestFlexTextUpdate(keyArg);
|
|
@@ -29721,7 +29796,7 @@ export class OpenCodeController {
|
|
|
29721
29796
|
{
|
|
29722
29797
|
projectId: eventArg.scopeId,
|
|
29723
29798
|
update,
|
|
29724
|
-
|
|
29799
|
+
...flexLiveTextAccounting(update.text),
|
|
29725
29800
|
},
|
|
29726
29801
|
eventBytes,
|
|
29727
29802
|
);
|
|
@@ -29788,7 +29863,7 @@ export class OpenCodeController {
|
|
|
29788
29863
|
{
|
|
29789
29864
|
projectId: eventArg.scopeId,
|
|
29790
29865
|
update,
|
|
29791
|
-
|
|
29866
|
+
...flexLiveTextAccounting(update.text),
|
|
29792
29867
|
},
|
|
29793
29868
|
eventBytes,
|
|
29794
29869
|
);
|
|
@@ -29836,13 +29911,11 @@ export class OpenCodeController {
|
|
|
29836
29911
|
this.advanceFlexMessageStreamEpoch(true);
|
|
29837
29912
|
return;
|
|
29838
29913
|
}
|
|
29839
|
-
const
|
|
29840
|
-
|
|
29841
|
-
entry.textUtf8Bytes,
|
|
29842
|
-
eventArg.delta,
|
|
29843
|
-
);
|
|
29914
|
+
const accounting = flexLiveTextAccountingAfterAppend(entry, eventArg.delta);
|
|
29915
|
+
const textUtf8Bytes = accounting?.textUtf8Bytes;
|
|
29844
29916
|
if (
|
|
29845
|
-
|
|
29917
|
+
accounting === undefined
|
|
29918
|
+
|| textUtf8Bytes === undefined
|
|
29846
29919
|
|| textUtf8Bytes <= entry.textUtf8Bytes
|
|
29847
29920
|
|| textUtf8Bytes !== eventArg.textUtf8Bytes
|
|
29848
29921
|
) {
|
|
@@ -29851,9 +29924,11 @@ export class OpenCodeController {
|
|
|
29851
29924
|
}
|
|
29852
29925
|
|
|
29853
29926
|
const revision = this.flexMessageStreamRevision + 1;
|
|
29927
|
+
// Measured with empty text first: the snapshot's size is that frame plus the text's tracked JSON bytes, so
|
|
29928
|
+
// the growing text is serialized only when a snapshot is served.
|
|
29854
29929
|
const update = {
|
|
29855
29930
|
...previous,
|
|
29856
|
-
text:
|
|
29931
|
+
text: '',
|
|
29857
29932
|
sourceUpdatedAt,
|
|
29858
29933
|
revision,
|
|
29859
29934
|
streamEpoch: this.flexMessageStreamEpoch,
|
|
@@ -29915,7 +29990,7 @@ export class OpenCodeController {
|
|
|
29915
29990
|
timestamp: sourceUpdatedAt,
|
|
29916
29991
|
};
|
|
29917
29992
|
const eventBytes = Buffer.byteLength(JSON.stringify(controllerEvent), 'utf8');
|
|
29918
|
-
const snapshotBytes = Buffer.byteLength(JSON.stringify(snapshotEvent), 'utf8');
|
|
29993
|
+
const snapshotBytes = Buffer.byteLength(JSON.stringify(snapshotEvent), 'utf8') + accounting.textJsonBytes;
|
|
29919
29994
|
if (
|
|
29920
29995
|
eventBytes > controllerMaxLiveMessageEventBytes
|
|
29921
29996
|
|| snapshotBytes > controllerMaxLiveMessageEventBytes
|
|
@@ -29926,6 +30001,7 @@ export class OpenCodeController {
|
|
|
29926
30001
|
return;
|
|
29927
30002
|
}
|
|
29928
30003
|
|
|
30004
|
+
update.text = `${previous.text}${eventArg.delta}`;
|
|
29929
30005
|
this.flexMessageStreamRevision = revision;
|
|
29930
30006
|
if (eventArg.partType === 'reasoning') {
|
|
29931
30007
|
this.setLatestFlexReasoningUpdate(
|
|
@@ -29933,7 +30009,7 @@ export class OpenCodeController {
|
|
|
29933
30009
|
{
|
|
29934
30010
|
projectId: eventArg.scopeId,
|
|
29935
30011
|
update: update as IControllerReasoningUpdate,
|
|
29936
|
-
|
|
30012
|
+
...accounting,
|
|
29937
30013
|
},
|
|
29938
30014
|
snapshotBytes,
|
|
29939
30015
|
);
|
|
@@ -29943,7 +30019,7 @@ export class OpenCodeController {
|
|
|
29943
30019
|
{
|
|
29944
30020
|
projectId: eventArg.scopeId,
|
|
29945
30021
|
update: update as IControllerTextUpdate,
|
|
29946
|
-
|
|
30022
|
+
...accounting,
|
|
29947
30023
|
},
|
|
29948
30024
|
snapshotBytes,
|
|
29949
30025
|
);
|
|
@@ -30634,7 +30710,7 @@ export class OpenCodeController {
|
|
|
30634
30710
|
{
|
|
30635
30711
|
projectId: eventArg.projectId,
|
|
30636
30712
|
update,
|
|
30637
|
-
|
|
30713
|
+
...flexLiveTextAccounting(update.text),
|
|
30638
30714
|
},
|
|
30639
30715
|
Buffer.byteLength(JSON.stringify(event), 'utf8'),
|
|
30640
30716
|
);
|
|
@@ -30662,7 +30738,7 @@ export class OpenCodeController {
|
|
|
30662
30738
|
{
|
|
30663
30739
|
projectId: eventArg.projectId,
|
|
30664
30740
|
update,
|
|
30665
|
-
|
|
30741
|
+
...flexLiveTextAccounting(update.text),
|
|
30666
30742
|
},
|
|
30667
30743
|
Buffer.byteLength(JSON.stringify(event), 'utf8'),
|
|
30668
30744
|
);
|
|
@@ -35074,20 +35150,44 @@ export class OpenCodeController {
|
|
|
35074
35150
|
));
|
|
35075
35151
|
}
|
|
35076
35152
|
|
|
35153
|
+
/** Every browser's own ordered event queue (`ControllerEventDelivery`), created with the first event. */
|
|
35154
|
+
private controllerEventDeliveryQueues(): ControllerEventDelivery {
|
|
35155
|
+
this.controllerEventDelivery ??= new ControllerEventDelivery({
|
|
35156
|
+
viewers: async () => {
|
|
35157
|
+
const typedSocket = this.typedServer?.typedsocket;
|
|
35158
|
+
if (!typedSocket) return [];
|
|
35159
|
+
const targets = await this.controllerEventViewers();
|
|
35160
|
+
return targets.map((targetArg) => ({
|
|
35161
|
+
peerId: targetArg.peer.id,
|
|
35162
|
+
send: async (eventArg: IControllerEvent) => {
|
|
35163
|
+
await typedSocket.createTypedRequest<IReq_ControllerEvent>(
|
|
35164
|
+
'controller.event',
|
|
35165
|
+
targetArg,
|
|
35166
|
+
{ timeoutMs: 2_000 },
|
|
35167
|
+
).fire(eventArg, { timeoutMs: 2_000, maxRetries: 0 });
|
|
35168
|
+
},
|
|
35169
|
+
}));
|
|
35170
|
+
},
|
|
35171
|
+
eventKey: (eventArg) => this.controllerEventKey(eventArg),
|
|
35172
|
+
mergeDelta: mergeControllerMessageDeltaEvents,
|
|
35173
|
+
limits: {
|
|
35174
|
+
events: maxPendingControllerEventsPerViewer,
|
|
35175
|
+
bytes: maxPendingControllerEventBytesPerViewer,
|
|
35176
|
+
mergedDeltaBytes: controllerMaxLiveMessageEventBytes,
|
|
35177
|
+
},
|
|
35178
|
+
});
|
|
35179
|
+
return this.controllerEventDelivery;
|
|
35180
|
+
}
|
|
35181
|
+
|
|
35182
|
+
/**
|
|
35183
|
+
* The driver and the session feed see every event once, in order, before any browser; each browser then gets it
|
|
35184
|
+
* through its own queue, so one that acknowledges slowly delays only itself. Resolves once every browser's queue
|
|
35185
|
+
* holds the event, never on an acknowledgement.
|
|
35186
|
+
*/
|
|
35077
35187
|
private async pushControllerEvent(eventArg: IControllerEvent): Promise<void> {
|
|
35078
35188
|
this.driverService?.observe(eventArg);
|
|
35079
35189
|
this.sessionFeed?.observe(eventArg);
|
|
35080
|
-
|
|
35081
|
-
if (!typedSocket) return;
|
|
35082
|
-
const targets = await this.controllerEventViewers();
|
|
35083
|
-
await Promise.allSettled(targets.map(async (targetArg) => {
|
|
35084
|
-
const request = typedSocket.createTypedRequest<IReq_ControllerEvent>(
|
|
35085
|
-
'controller.event',
|
|
35086
|
-
targetArg,
|
|
35087
|
-
{ timeoutMs: 2_000 },
|
|
35088
|
-
);
|
|
35089
|
-
await request.fire(eventArg, { timeoutMs: 2_000, maxRetries: 0 });
|
|
35090
|
-
}));
|
|
35190
|
+
await this.controllerEventDeliveryQueues().push(eventArg);
|
|
35091
35191
|
}
|
|
35092
35192
|
|
|
35093
35193
|
private runUpgradePreparationAdmission<T>(operationArg: () => Promise<T>): Promise<T> {
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { IControllerEvent } from '../ts_interfaces/interfaces.js';
|
|
2
|
+
|
|
3
|
+
/** One authenticated browser connection that receives controller events. */
|
|
4
|
+
export interface IControllerEventViewer {
|
|
5
|
+
peerId: string;
|
|
6
|
+
/** Sends one event; resolves on the browser's acknowledgement, rejects on its timeout or a closed connection. */
|
|
7
|
+
send(eventArg: IControllerEvent): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface IControllerEventDeliveryLimits {
|
|
11
|
+
/** Events one viewer may have waiting before its queue collapses into a resync. */
|
|
12
|
+
events: number;
|
|
13
|
+
/** Serialized bytes one viewer may have waiting before its queue collapses into a resync. */
|
|
14
|
+
bytes: number;
|
|
15
|
+
/** The largest live message delta a merge in a viewer's queue may produce. */
|
|
16
|
+
mergedDeltaBytes: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IControllerEventDeliveryOptions {
|
|
20
|
+
/** The connections that receive events now; read once per batch of pushed events. */
|
|
21
|
+
viewers(): Promise<IControllerEventViewer[]>;
|
|
22
|
+
/** Events with one key carry the same state, so in a collapsed queue the newest replaces the older ones. */
|
|
23
|
+
eventKey(eventArg: IControllerEvent): string;
|
|
24
|
+
/** Merges a contiguous live message delta into the delta queued just before it, or refuses with `undefined`. */
|
|
25
|
+
mergeDelta(previousArg: IControllerEvent, eventArg: IControllerEvent): IControllerEvent | undefined;
|
|
26
|
+
limits: IControllerEventDeliveryLimits;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface IQueuedControllerEvent {
|
|
30
|
+
event: IControllerEvent;
|
|
31
|
+
/** `JSON.stringify(event)` bytes; for a merged delta the sum of its parts, never less than its size. */
|
|
32
|
+
bytes: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface IViewerQueue {
|
|
36
|
+
viewer: IControllerEventViewer;
|
|
37
|
+
/** Waiting events in send order. Normal keys are a sequence; a collapsed queue keys by state. */
|
|
38
|
+
events: Map<string, IQueuedControllerEvent>;
|
|
39
|
+
/** The key of the newest waiting event while the queue is not collapsed: where a delta may merge. */
|
|
40
|
+
tailKey?: string;
|
|
41
|
+
bytes: number;
|
|
42
|
+
/** Set when the queue overflowed and until it drains: events then replace older ones of their state. */
|
|
43
|
+
collapsed: boolean;
|
|
44
|
+
sending?: Promise<void>;
|
|
45
|
+
removed: boolean;
|
|
46
|
+
/** How many times this queue collapsed. */
|
|
47
|
+
collapses: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface IPendingFanOut {
|
|
51
|
+
item: IQueuedControllerEvent;
|
|
52
|
+
delivered: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const isLiveEvent = (eventArg: IControllerEvent): boolean => Boolean(
|
|
56
|
+
eventArg.toolExecution
|
|
57
|
+
|| eventArg.reasoningUpdate
|
|
58
|
+
|| eventArg.reasoningDelta
|
|
59
|
+
|| eventArg.textUpdate
|
|
60
|
+
|| eventArg.textDelta,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/** The live stream content in one event, which only a delta's own successor or a re-read can replace. */
|
|
64
|
+
const isMessageDelta = (eventArg: IControllerEvent): boolean => Boolean(eventArg.reasoningDelta || eventArg.textDelta);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Delivers controller events to every viewer through its own bounded, ordered queue, so a viewer that acknowledges
|
|
68
|
+
* slowly or never delays only itself. Each viewer has at most one event in flight and receives events in push order.
|
|
69
|
+
*
|
|
70
|
+
* A viewer whose queue passes its limits is resynchronized alone, with the existing Flex queue's collapse rules:
|
|
71
|
+
* the queue keeps the newest event of each state key (statuses, barriers, feed and ticket revisions, settings),
|
|
72
|
+
* drops live tool, reasoning and text content, and ends with `sessions.changed` for each harness whose live content
|
|
73
|
+
* it dropped, which makes the browser re-read the conversations and the open session (live snapshots included).
|
|
74
|
+
* The viewer's own revision checks catch any delta that no longer fits. Should a collapsed queue still hold more
|
|
75
|
+
* than its event limit, each session's states fold into their harness's `sessions.changed` too, so what remains is
|
|
76
|
+
* one event per harness and per project or global state. The queue stays collapsed until it drains.
|
|
77
|
+
*/
|
|
78
|
+
export class ControllerEventDelivery {
|
|
79
|
+
private readonly queues = new Map<string, IViewerQueue>();
|
|
80
|
+
private readonly fanOutQueue: IPendingFanOut[] = [];
|
|
81
|
+
private fanOutTask?: Promise<void>;
|
|
82
|
+
private sequence = 0;
|
|
83
|
+
private generation = 0;
|
|
84
|
+
|
|
85
|
+
constructor(private readonly options: IControllerEventDeliveryOptions) {}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Queues one event for every current viewer, in push order. Resolves once each viewer's queue holds it; it never
|
|
89
|
+
* waits for an acknowledgement.
|
|
90
|
+
*/
|
|
91
|
+
public push(eventArg: IControllerEvent): Promise<void> {
|
|
92
|
+
const item: IQueuedControllerEvent = {
|
|
93
|
+
event: eventArg,
|
|
94
|
+
bytes: Buffer.byteLength(JSON.stringify(eventArg), 'utf8'),
|
|
95
|
+
};
|
|
96
|
+
return new Promise<void>((resolve) => {
|
|
97
|
+
this.fanOutQueue.push({ item, delivered: resolve });
|
|
98
|
+
this.startFanOut();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The events waiting for one viewer, in send order (the one in flight excluded). */
|
|
103
|
+
public waiting(peerIdArg: string): IControllerEvent[] {
|
|
104
|
+
return [...(this.queues.get(peerIdArg)?.events.values() ?? [])].map((itemArg) => itemArg.event);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** How many times the queue of a connected viewer collapsed into a resync. */
|
|
108
|
+
public collapseCount(peerIdArg: string): number {
|
|
109
|
+
return this.queues.get(peerIdArg)?.collapses ?? 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Drops every waiting event and waits for the sends in flight; later pushes start fresh queues. */
|
|
113
|
+
public async stop(): Promise<void> {
|
|
114
|
+
this.generation += 1;
|
|
115
|
+
const sending: Promise<void>[] = [];
|
|
116
|
+
for (const queue of this.queues.values()) {
|
|
117
|
+
queue.removed = true;
|
|
118
|
+
queue.events.clear();
|
|
119
|
+
queue.bytes = 0;
|
|
120
|
+
if (queue.sending) sending.push(queue.sending);
|
|
121
|
+
}
|
|
122
|
+
this.queues.clear();
|
|
123
|
+
if (this.fanOutTask) sending.push(this.fanOutTask);
|
|
124
|
+
await Promise.allSettled(sending);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private startFanOut(): void {
|
|
128
|
+
if (this.fanOutTask) return;
|
|
129
|
+
const task = this.fanOut().finally(() => {
|
|
130
|
+
if (this.fanOutTask === task) this.fanOutTask = undefined;
|
|
131
|
+
if (this.fanOutQueue.length > 0) this.startFanOut();
|
|
132
|
+
});
|
|
133
|
+
this.fanOutTask = task;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async fanOut(): Promise<void> {
|
|
137
|
+
while (this.fanOutQueue.length > 0) {
|
|
138
|
+
const batch = this.fanOutQueue.splice(0);
|
|
139
|
+
const generation = this.generation;
|
|
140
|
+
let viewers: IControllerEventViewer[] = [];
|
|
141
|
+
try {
|
|
142
|
+
viewers = await this.options.viewers();
|
|
143
|
+
} catch {
|
|
144
|
+
// No viewer can be named now; the events reach none, as a push without connections.
|
|
145
|
+
}
|
|
146
|
+
if (generation === this.generation) {
|
|
147
|
+
const current = new Set(viewers.map((viewerArg) => viewerArg.peerId));
|
|
148
|
+
for (const [peerId, queue] of this.queues) {
|
|
149
|
+
if (current.has(peerId)) continue;
|
|
150
|
+
queue.removed = true;
|
|
151
|
+
queue.events.clear();
|
|
152
|
+
queue.bytes = 0;
|
|
153
|
+
this.queues.delete(peerId);
|
|
154
|
+
}
|
|
155
|
+
for (const viewer of viewers) {
|
|
156
|
+
let queue = this.queues.get(viewer.peerId);
|
|
157
|
+
if (!queue) {
|
|
158
|
+
queue = { viewer, events: new Map(), bytes: 0, collapsed: false, removed: false, collapses: 0 };
|
|
159
|
+
this.queues.set(viewer.peerId, queue);
|
|
160
|
+
}
|
|
161
|
+
queue.viewer = viewer;
|
|
162
|
+
for (const pending of batch) this.enqueue(queue, pending.item);
|
|
163
|
+
this.startSending(queue);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const pending of batch) pending.delivered();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private enqueue(queueArg: IViewerQueue, itemArg: IQueuedControllerEvent): void {
|
|
171
|
+
if (queueArg.collapsed) {
|
|
172
|
+
this.absorb(queueArg, itemArg);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (isMessageDelta(itemArg.event) && queueArg.tailKey !== undefined) {
|
|
176
|
+
const tailKey = queueArg.tailKey;
|
|
177
|
+
const tail = queueArg.events.get(tailKey);
|
|
178
|
+
if (
|
|
179
|
+
tail !== undefined
|
|
180
|
+
&& isMessageDelta(tail.event)
|
|
181
|
+
&& tail.bytes + itemArg.bytes <= this.options.limits.mergedDeltaBytes
|
|
182
|
+
) {
|
|
183
|
+
const merged = this.options.mergeDelta(tail.event, itemArg.event);
|
|
184
|
+
if (merged) {
|
|
185
|
+
queueArg.events.set(tailKey, { event: merged, bytes: tail.bytes + itemArg.bytes });
|
|
186
|
+
queueArg.bytes += itemArg.bytes;
|
|
187
|
+
if (queueArg.bytes > this.options.limits.bytes) this.collapse(queueArg);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
queueArg.events.size + 1 > this.options.limits.events
|
|
194
|
+
|| queueArg.bytes + itemArg.bytes > this.options.limits.bytes
|
|
195
|
+
) {
|
|
196
|
+
this.collapse(queueArg);
|
|
197
|
+
this.absorb(queueArg, itemArg);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.sequence += 1;
|
|
201
|
+
queueArg.tailKey = `#${this.sequence}`;
|
|
202
|
+
queueArg.events.set(queueArg.tailKey, itemArg);
|
|
203
|
+
queueArg.bytes += itemArg.bytes;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private collapse(queueArg: IViewerQueue): void {
|
|
207
|
+
const dropped = [...queueArg.events.values()];
|
|
208
|
+
queueArg.events.clear();
|
|
209
|
+
queueArg.tailKey = undefined;
|
|
210
|
+
queueArg.bytes = 0;
|
|
211
|
+
queueArg.collapsed = true;
|
|
212
|
+
queueArg.collapses += 1;
|
|
213
|
+
for (const item of dropped) this.absorb(queueArg, item);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Adds an event to a collapsed queue: the newest of its state, or a resync of its harness for live content. */
|
|
217
|
+
private absorb(queueArg: IViewerQueue, itemArg: IQueuedControllerEvent): void {
|
|
218
|
+
if (isLiveEvent(itemArg.event)) {
|
|
219
|
+
this.resyncHarness(queueArg, itemArg.event);
|
|
220
|
+
} else {
|
|
221
|
+
this.place(queueArg, `state\0${this.options.eventKey(itemArg.event)}`, itemArg);
|
|
222
|
+
}
|
|
223
|
+
if (queueArg.events.size > this.options.limits.events) this.foldSessions(queueArg);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A collapsed queue that still holds more states than its limit keeps only what is not about one session: each
|
|
228
|
+
* session's states become their harness's `sessions.changed`, whose re-read covers the open session and the list.
|
|
229
|
+
*/
|
|
230
|
+
private foldSessions(queueArg: IViewerQueue): void {
|
|
231
|
+
for (const [key, item] of [...queueArg.events]) {
|
|
232
|
+
if (!key.startsWith('state\0') || item.event.sessionId === undefined) continue;
|
|
233
|
+
queueArg.events.delete(key);
|
|
234
|
+
queueArg.bytes -= item.bytes;
|
|
235
|
+
this.resyncHarness(queueArg, item.event);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private resyncHarness(queueArg: IViewerQueue, eventArg: IControllerEvent): void {
|
|
240
|
+
const harnessId = eventArg.harnessId;
|
|
241
|
+
const event: IControllerEvent = {
|
|
242
|
+
type: 'sessions.changed',
|
|
243
|
+
...(harnessId === undefined ? {} : { harnessId }),
|
|
244
|
+
timestamp: eventArg.timestamp,
|
|
245
|
+
};
|
|
246
|
+
this.place(
|
|
247
|
+
queueArg,
|
|
248
|
+
`resync\0${harnessId ?? ''}`,
|
|
249
|
+
{ event, bytes: Buffer.byteLength(JSON.stringify(event), 'utf8') },
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Puts an event last under its key, replacing an older one with that key. */
|
|
254
|
+
private place(queueArg: IViewerQueue, keyArg: string, itemArg: IQueuedControllerEvent): void {
|
|
255
|
+
const replaced = queueArg.events.get(keyArg);
|
|
256
|
+
if (replaced) {
|
|
257
|
+
queueArg.events.delete(keyArg);
|
|
258
|
+
queueArg.bytes -= replaced.bytes;
|
|
259
|
+
}
|
|
260
|
+
queueArg.events.set(keyArg, itemArg);
|
|
261
|
+
queueArg.bytes += itemArg.bytes;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private startSending(queueArg: IViewerQueue): void {
|
|
265
|
+
if (queueArg.sending || queueArg.events.size === 0) return;
|
|
266
|
+
const task = this.send(queueArg).finally(() => {
|
|
267
|
+
if (queueArg.sending === task) queueArg.sending = undefined;
|
|
268
|
+
if (!queueArg.removed && queueArg.events.size > 0) this.startSending(queueArg);
|
|
269
|
+
});
|
|
270
|
+
queueArg.sending = task;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private async send(queueArg: IViewerQueue): Promise<void> {
|
|
274
|
+
while (!queueArg.removed) {
|
|
275
|
+
const next = queueArg.events.entries().next().value as [string, IQueuedControllerEvent] | undefined;
|
|
276
|
+
if (!next) return;
|
|
277
|
+
queueArg.events.delete(next[0]);
|
|
278
|
+
queueArg.bytes -= next[1].bytes;
|
|
279
|
+
if (queueArg.tailKey === next[0]) queueArg.tailKey = undefined;
|
|
280
|
+
if (queueArg.events.size === 0) queueArg.collapsed = false;
|
|
281
|
+
try {
|
|
282
|
+
await queueArg.viewer.send(next[1].event);
|
|
283
|
+
} catch {
|
|
284
|
+
// As before: a viewer that does not acknowledge in time misses nothing it already received, and a closed
|
|
285
|
+
// connection leaves the viewer set at the next push.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -727,6 +727,16 @@ export class FlexPublicHeadModel extends plugins.nosqldb.SmartDataDbDoc<
|
|
|
727
727
|
name: 'flex_public_session_scope_purge',
|
|
728
728
|
key: { controllerId: 1, scopeId: 1, id: 1 },
|
|
729
729
|
})
|
|
730
|
+
/**
|
|
731
|
+
* One session's records in a scope: a reader finds the record a publication lists for a session
|
|
732
|
+
* among that session's versions, never among every session's records. Created by the startup
|
|
733
|
+
* init of the models (`initFlexPublicModels`, the projection reader's `init`), outside any
|
|
734
|
+
* transaction. An older AGL leaves the index it does not declare alone.
|
|
735
|
+
*/
|
|
736
|
+
@plugins.nosqldb.compoundIndex({
|
|
737
|
+
name: 'flex_public_session_lookup',
|
|
738
|
+
key: { controllerId: 1, scopeId: 1, 'session.sessionId': 1 },
|
|
739
|
+
})
|
|
730
740
|
@plugins.nosqldb.managed({ collectionName: flexPublicSessionsCollectionName })
|
|
731
741
|
export class FlexPublicSessionRecordModel extends plugins.nosqldb.SmartDataDbDoc<
|
|
732
742
|
FlexPublicSessionRecordModel,
|
|
@@ -414,9 +414,7 @@ export class FlexProjectionReader {
|
|
|
414
414
|
signalArg?: AbortSignal,
|
|
415
415
|
): Promise<TFlexSession> {
|
|
416
416
|
const selection = await this.selectCandidate(scopeIdArg, undefined, signalArg);
|
|
417
|
-
const
|
|
418
|
-
const record = records.find((entry) => entry.session.sessionId === sessionIdArg);
|
|
419
|
-
if (!record) throw new FlexProjectionNotFoundError();
|
|
417
|
+
const record = await this.loadSessionRecord(selection, sessionIdArg, signalArg);
|
|
420
418
|
return JSON.parse(JSON.stringify(record.session)) as TFlexSession;
|
|
421
419
|
}
|
|
422
420
|
|
|
@@ -627,6 +625,35 @@ export class FlexProjectionReader {
|
|
|
627
625
|
}).sort(compareNewestSession);
|
|
628
626
|
}
|
|
629
627
|
|
|
628
|
+
/**
|
|
629
|
+
* The record the selected publication lists for one session: looked up through the session
|
|
630
|
+
* index among the records the publication lists, so it reads that session's records only, and
|
|
631
|
+
* proven against its manifest.
|
|
632
|
+
*/
|
|
633
|
+
private async loadSessionRecord(
|
|
634
|
+
selectionArg: IFlexCandidateSelection,
|
|
635
|
+
sessionIdArg: string,
|
|
636
|
+
signalArg?: AbortSignal,
|
|
637
|
+
): Promise<IFlexPublicSessionRecordDocument> {
|
|
638
|
+
const sessionRecords = selectionArg.manifest.sessionRecords;
|
|
639
|
+
const models = await this.models.publicSessions.getOffsetPage({
|
|
640
|
+
controllerId: this.controllerId,
|
|
641
|
+
scopeId: selectionArg.head.storageKey,
|
|
642
|
+
'session.sessionId': sessionIdArg,
|
|
643
|
+
id: { $in: sessionRecords.map((record) => record.id) },
|
|
644
|
+
}, this.pageOptions(2, signalArg));
|
|
645
|
+
// A publication lists one record per session.
|
|
646
|
+
if (models.length > 1) throw new FlexProjectionFormatError();
|
|
647
|
+
const [model] = models;
|
|
648
|
+
if (!model) throw new FlexProjectionNotFoundError();
|
|
649
|
+
const document = projectSessionRecord(model);
|
|
650
|
+
assertFlexPublicSessionRecordDocument(document);
|
|
651
|
+
const manifest = sessionRecords.find((record) => record.id === document.id);
|
|
652
|
+
if (!manifest || document.session.sessionId !== sessionIdArg) throw new FlexProjectionFormatError();
|
|
653
|
+
verifyRecordManifest(document, manifest);
|
|
654
|
+
return document;
|
|
655
|
+
}
|
|
656
|
+
|
|
630
657
|
/**
|
|
631
658
|
* The transcript the selected publication names for a session, proven by its id; a session
|
|
632
659
|
* without messages has none.
|