agl 22.0.4 → 24.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 +21 -0
- package/dist_serve/bundle.js +2418 -2408
- package/dist_serve/bundle.js.map +1 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.authstore.js +2 -2
- package/dist_ts/classes.browserresourcehost.d.ts +1 -0
- package/dist_ts/classes.browserresourcehost.js +7 -1
- package/dist_ts/classes.controller.d.ts +37 -5
- package/dist_ts/classes.controller.js +1491 -249
- package/dist_ts/classes.flexservice.d.ts +5 -0
- package/dist_ts/classes.flexservice.js +131 -4
- package/dist_ts/classes.opencodeclient.d.ts +2 -0
- package/dist_ts/classes.opencodeclient.js +23 -1
- package/dist_ts/classes.validation.d.ts +9 -1
- package/dist_ts/classes.validation.js +62 -7
- package/dist_ts/interfaces.flexipc.d.ts +18 -1
- package/dist_ts/interfaces.flexipc.js +42 -8
- package/dist_ts_interfaces/browsertransport.d.ts +72 -0
- package/dist_ts_interfaces/browsertransport.js +47 -0
- package/dist_ts_interfaces/index.d.ts +1 -0
- package/dist_ts_interfaces/index.js +2 -1
- package/dist_ts_interfaces/interfaces.d.ts +42 -7
- package/dist_ts_interfaces/interfaces.js +5 -3
- package/dist_ts_interfaces/requests.d.ts +20 -21
- package/package.json +7 -7
- package/readme.md +12 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.authstore.ts +1 -1
- package/ts/classes.browserresourcehost.ts +7 -0
- package/ts/classes.controller.ts +1960 -305
- package/ts/classes.flexservice.ts +132 -2
- package/ts/classes.opencodeclient.ts +19 -0
- package/ts/classes.validation.ts +77 -6
- package/ts/interfaces.flexipc.ts +65 -7
- package/ts_interfaces/browsertransport.ts +126 -0
- package/ts_interfaces/index.ts +1 -0
- package/ts_interfaces/interfaces.ts +46 -11
- package/ts_interfaces/requests.ts +23 -30
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.browserviewtransport.ts +532 -0
- package/ts_web/classes.controllersocketclient.ts +119 -29
- package/ts_web/elements.harnesscontrollerapp.ts +1727 -287
- package/ts_web/index.ts +1 -0
- package/ts_web/plugins.ts +2 -0
package/ts/classes.controller.ts
CHANGED
|
@@ -12,6 +12,22 @@ import {
|
|
|
12
12
|
controllerBuiltinCommands,
|
|
13
13
|
controllerMaxToolEventBytes,
|
|
14
14
|
controllerMaxLiveMessageEventBytes,
|
|
15
|
+
controllerBrowserTransportEventContentType,
|
|
16
|
+
controllerBrowserTransportMaximumActiveAcknowledgements,
|
|
17
|
+
controllerBrowserTransportMaximumFrameBytes,
|
|
18
|
+
controllerBrowserTransportMaximumJsonBytes,
|
|
19
|
+
controllerBrowserTransportMaximumPendingEventBytes,
|
|
20
|
+
controllerBrowserTransportMaximumPendingEvents,
|
|
21
|
+
controllerBrowserTransportMaximumPendingOperations,
|
|
22
|
+
controllerBrowserTransportMaximumQueuedOperationBytes,
|
|
23
|
+
controllerBrowserTransportMaximumRecoveryAttempts,
|
|
24
|
+
controllerBrowserTransportOperationContentType,
|
|
25
|
+
controllerBrowserTransportPairOpenTimeoutMs,
|
|
26
|
+
controllerBrowserTransportProtocol,
|
|
27
|
+
controllerBrowserTransportRecoveryGraceMs,
|
|
28
|
+
decodeControllerBrowserTransportPacket,
|
|
29
|
+
encodeControllerBrowserTransportPacket,
|
|
30
|
+
controllerLiveMessageDeltaTargetBytes,
|
|
15
31
|
controllerInitialMessageBundleLimit,
|
|
16
32
|
controllerMaximumTranscriptBytes,
|
|
17
33
|
controllerLiveToolPayloadNotice,
|
|
@@ -26,7 +42,9 @@ import {
|
|
|
26
42
|
type IControllerMessagePage,
|
|
27
43
|
type IControllerMessageStreamCursor,
|
|
28
44
|
type IControllerPendingPrompt,
|
|
45
|
+
type IControllerReasoningDelta,
|
|
29
46
|
type IControllerReasoningUpdate,
|
|
47
|
+
type IControllerTextDelta,
|
|
30
48
|
type IControllerTextUpdate,
|
|
31
49
|
type IControllerTodo,
|
|
32
50
|
type TControllerModelChoice,
|
|
@@ -92,8 +110,15 @@ import {
|
|
|
92
110
|
type IReq_ControllerBrowserViewOpen,
|
|
93
111
|
type IReq_ControllerBrowserViewActivate,
|
|
94
112
|
type IReq_ControllerBrowserViewClose,
|
|
95
|
-
type
|
|
96
|
-
type
|
|
113
|
+
type IReq_ControllerBrowserViewRecover,
|
|
114
|
+
type IControllerBrowserFrame,
|
|
115
|
+
type IControllerBrowserFrameMetadata,
|
|
116
|
+
type IControllerBrowserViewState,
|
|
117
|
+
type IControllerBrowserOperationPacket,
|
|
118
|
+
type IControllerBrowserOperationResultPacket,
|
|
119
|
+
type TControllerBrowserClientPacket,
|
|
120
|
+
type TControllerBrowserServerPacket,
|
|
121
|
+
type TControllerBrowserViewOperation,
|
|
97
122
|
type IReq_ControllerProjectCreate,
|
|
98
123
|
type IReq_ControllerProjectList,
|
|
99
124
|
type IReq_ControllerProjectRemove,
|
|
@@ -239,7 +264,8 @@ import {
|
|
|
239
264
|
assertBrowserViewOpenRequest,
|
|
240
265
|
assertBrowserViewActivateRequest,
|
|
241
266
|
assertBrowserViewCloseRequest,
|
|
242
|
-
|
|
267
|
+
assertBrowserViewRecoverRequest,
|
|
268
|
+
assertControllerBrowserClientPacket,
|
|
243
269
|
assertProjectCreateRequest,
|
|
244
270
|
resolveProjectPathInput,
|
|
245
271
|
assertProjectRemoveRequest,
|
|
@@ -329,9 +355,12 @@ const trustedProxySecretHeader = 'x-harness-controller-proxy-secret';
|
|
|
329
355
|
const websocketPayloadLimit = 16 * 1024 * 1024;
|
|
330
356
|
const browserViewActivationTimeoutMs = 15_000;
|
|
331
357
|
const browserViewCleanupRetryMs = 1_000;
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
const
|
|
358
|
+
const browserViewTransportCleanupTimeoutMs = 9_000;
|
|
359
|
+
const browserViewRealtimePeerRate = 120;
|
|
360
|
+
const browserViewRealtimePeerBurst = 240;
|
|
361
|
+
const browserViewRealtimeGlobalRate = 512;
|
|
362
|
+
const browserViewRealtimeGlobalBurst = 1_024;
|
|
363
|
+
const browserStreamAuthorizationMarker = Symbol('controller.browser.stream');
|
|
335
364
|
const browserSemanticOperationClassifications: ReadonlySet<
|
|
336
365
|
plugins.browserRuntime.TBrowserRuntimeOperationClassification
|
|
337
366
|
> = new Set(['navigation', 'tab', 'agent-action']);
|
|
@@ -473,22 +502,122 @@ interface ITerminalPeerLifecycleState {
|
|
|
473
502
|
connected: boolean;
|
|
474
503
|
}
|
|
475
504
|
|
|
505
|
+
class ControllerBrowserTokenBucket {
|
|
506
|
+
private tokens: number;
|
|
507
|
+
private updatedAt = performance.now();
|
|
508
|
+
|
|
509
|
+
constructor(
|
|
510
|
+
private readonly refillPerSecond: number,
|
|
511
|
+
private readonly capacity: number,
|
|
512
|
+
) {
|
|
513
|
+
this.tokens = capacity;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
public consume(nowArg = performance.now()): boolean {
|
|
517
|
+
const elapsedMs = Math.max(0, nowArg - this.updatedAt);
|
|
518
|
+
this.updatedAt = nowArg;
|
|
519
|
+
this.tokens = Math.min(
|
|
520
|
+
this.capacity,
|
|
521
|
+
this.tokens + ((elapsedMs / 1_000) * this.refillPerSecond),
|
|
522
|
+
);
|
|
523
|
+
if (this.tokens < 1) return false;
|
|
524
|
+
this.tokens -= 1;
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
type TControllerBrowserViewLifecycle =
|
|
530
|
+
| 'inactive'
|
|
531
|
+
| 'activating'
|
|
532
|
+
| 'active'
|
|
533
|
+
| 'recoverable'
|
|
534
|
+
| 'recovering'
|
|
535
|
+
| 'failing'
|
|
536
|
+
| 'closing';
|
|
537
|
+
|
|
538
|
+
type TControllerBrowserTransportLifecycle =
|
|
539
|
+
| 'opening'
|
|
540
|
+
| 'ready'
|
|
541
|
+
| 'active'
|
|
542
|
+
| 'failed'
|
|
543
|
+
| 'retiring'
|
|
544
|
+
| 'retired';
|
|
545
|
+
|
|
546
|
+
interface IControllerBrowserQueuedOperation {
|
|
547
|
+
operationId: number;
|
|
548
|
+
operation: TControllerBrowserViewOperation;
|
|
549
|
+
byteLength: number;
|
|
550
|
+
abortController?: AbortController;
|
|
551
|
+
cancelled: boolean;
|
|
552
|
+
terminal: boolean;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
interface IControllerBrowserAcknowledgementOperation {
|
|
556
|
+
operationId: number;
|
|
557
|
+
terminal: boolean;
|
|
558
|
+
operation: Promise<void>;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
interface IControllerBrowserViewTransport {
|
|
562
|
+
readonly identity: symbol;
|
|
563
|
+
generation: number;
|
|
564
|
+
lifecycle: TControllerBrowserTransportLifecycle;
|
|
565
|
+
events: plugins.typedrequestInterfaces.TVirtualStream<'send'>;
|
|
566
|
+
operations: plugins.typedrequestInterfaces.TVirtualStream<'receive'>;
|
|
567
|
+
abortController: AbortController;
|
|
568
|
+
openingTask: Promise<void>;
|
|
569
|
+
receiveTask?: Promise<void>;
|
|
570
|
+
drainTask?: Promise<void>;
|
|
571
|
+
deliveryTail: Promise<void>;
|
|
572
|
+
pendingEventCount: number;
|
|
573
|
+
pendingEventBytes: number;
|
|
574
|
+
operationQueue: IControllerBrowserQueuedOperation[];
|
|
575
|
+
queuedOperationBytes: number;
|
|
576
|
+
highestOperationId: number;
|
|
577
|
+
cancellableOperationIds: Set<number>;
|
|
578
|
+
activeMutation?: IControllerBrowserQueuedOperation;
|
|
579
|
+
activeAcknowledgements: Map<number, IControllerBrowserAcknowledgementOperation>;
|
|
580
|
+
retirementTask?: Promise<void>;
|
|
581
|
+
retirementSettlement?: Promise<void>;
|
|
582
|
+
retirementSettled?: boolean;
|
|
583
|
+
retirementDeadlineExpired?: boolean;
|
|
584
|
+
failure?: unknown;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
interface IControllerBrowserTransportConstructionCleanup {
|
|
588
|
+
readonly settlement: Promise<void>;
|
|
589
|
+
readonly reason: unknown;
|
|
590
|
+
settled: boolean;
|
|
591
|
+
deadlineExpired: boolean;
|
|
592
|
+
}
|
|
593
|
+
|
|
476
594
|
interface IControllerBrowserView {
|
|
477
595
|
id: string;
|
|
478
596
|
peerId: string;
|
|
597
|
+
credentialId: string;
|
|
479
598
|
transportConnection: plugins.typedsocket.ISmartServeConnectionWrapper;
|
|
480
599
|
transportPeer: plugins.typedsocket.ISmartServeConnectionWrapper['peer'];
|
|
481
600
|
resource: IControllerResourceDocument;
|
|
482
601
|
handle: Awaited<ReturnType<ControllerBrowserResourceHost['openHumanView']>>;
|
|
483
602
|
authority: plugins.browserRuntime.TBrowserRuntimeLeaseAuthority;
|
|
484
|
-
|
|
603
|
+
lifecycle: TControllerBrowserViewLifecycle;
|
|
485
604
|
activationAbortController: AbortController;
|
|
486
605
|
activationTimer?: ReturnType<typeof setTimeout>;
|
|
487
606
|
activationPromise?: Promise<void>;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
607
|
+
transport?: IControllerBrowserViewTransport;
|
|
608
|
+
pendingTransportIdentity?: symbol;
|
|
609
|
+
transportConstructionCleanup?: IControllerBrowserTransportConstructionCleanup;
|
|
610
|
+
streamGeneration: number;
|
|
611
|
+
stateRevision: number;
|
|
612
|
+
recoveryAttempts: number;
|
|
613
|
+
recoveryDeadlineAt?: number;
|
|
614
|
+
recoveryTimer?: ReturnType<typeof setTimeout>;
|
|
615
|
+
recoveryFrameGate?: {
|
|
616
|
+
tabId: string;
|
|
617
|
+
minimumGeneration: number;
|
|
618
|
+
viewportRevision: number;
|
|
619
|
+
admitted: boolean;
|
|
620
|
+
};
|
|
492
621
|
closing: boolean;
|
|
493
622
|
cleanupTask?: Promise<void>;
|
|
494
623
|
cleanupRetryTimer?: ReturnType<typeof setTimeout>;
|
|
@@ -500,13 +629,15 @@ interface IControllerBrowserViewSlot {
|
|
|
500
629
|
view?: IControllerBrowserView;
|
|
501
630
|
}
|
|
502
631
|
|
|
503
|
-
interface
|
|
504
|
-
readonly marker: typeof
|
|
632
|
+
interface IControllerBrowserStreamAuthorization {
|
|
633
|
+
readonly marker: typeof browserStreamAuthorizationMarker;
|
|
505
634
|
readonly view: IControllerBrowserView;
|
|
506
635
|
readonly authority: plugins.browserRuntime.TBrowserRuntimeLeaseAuthority;
|
|
507
636
|
readonly transportPeer: plugins.typedsocket.ISmartServeConnectionWrapper['peer'];
|
|
637
|
+
readonly transportIdentity: symbol;
|
|
638
|
+
readonly streamGeneration: number;
|
|
639
|
+
readonly creatorDirection: plugins.typedrequestInterfaces.TVirtualStreamDirection;
|
|
508
640
|
readonly contentType: string;
|
|
509
|
-
readonly integrity: plugins.typedrequestInterfaces.IVirtualStreamIntegrity;
|
|
510
641
|
}
|
|
511
642
|
|
|
512
643
|
interface IFlexRefreshJobCacheEntry {
|
|
@@ -540,15 +671,24 @@ interface ILatestToolExecution {
|
|
|
540
671
|
interface ILatestReasoningUpdate {
|
|
541
672
|
projectId: string;
|
|
542
673
|
update: IControllerReasoningUpdate;
|
|
674
|
+
textUtf8Bytes: number;
|
|
543
675
|
bytes?: number;
|
|
544
676
|
}
|
|
545
677
|
|
|
546
678
|
interface ILatestTextUpdate {
|
|
547
679
|
projectId: string;
|
|
548
680
|
update: IControllerTextUpdate;
|
|
681
|
+
textUtf8Bytes: number;
|
|
549
682
|
bytes?: number;
|
|
550
683
|
}
|
|
551
684
|
|
|
685
|
+
type TControllerMessageDelta = IControllerReasoningDelta | IControllerTextDelta;
|
|
686
|
+
|
|
687
|
+
interface IControllerTextDeltaFragment {
|
|
688
|
+
delta: string;
|
|
689
|
+
utf8Bytes: number;
|
|
690
|
+
}
|
|
691
|
+
|
|
552
692
|
type TControllerToolExecutionSnapshot = Omit<
|
|
553
693
|
IControllerToolExecution,
|
|
554
694
|
'revision' | 'streamEpoch'
|
|
@@ -558,6 +698,103 @@ const historyLimitedNotice = '[earlier message content omitted: this transcript
|
|
|
558
698
|
const historyLimitedDigestPrefix = 'history-limited:';
|
|
559
699
|
const liveToolExecutionBytes = controllerMaxToolEventBytes - (8 * 1024);
|
|
560
700
|
|
|
701
|
+
const controllerEventMessageDelta = (
|
|
702
|
+
eventArg: IControllerEvent,
|
|
703
|
+
): TControllerMessageDelta | undefined => eventArg.reasoningDelta ?? eventArg.textDelta;
|
|
704
|
+
|
|
705
|
+
const controllerTextAfterAppendUtf8Bytes = (
|
|
706
|
+
textArg: string,
|
|
707
|
+
textUtf8BytesArg: number,
|
|
708
|
+
deltaArg: string,
|
|
709
|
+
): number | undefined => {
|
|
710
|
+
// Separately encoded surrogate halves cost six bytes; after append the pair costs four.
|
|
711
|
+
const boundaryCorrection = (
|
|
712
|
+
textArg.length > 0
|
|
713
|
+
&& deltaArg.length > 0
|
|
714
|
+
&& textArg.charCodeAt(textArg.length - 1) >= 0xD800
|
|
715
|
+
&& textArg.charCodeAt(textArg.length - 1) <= 0xDBFF
|
|
716
|
+
&& deltaArg.charCodeAt(0) >= 0xDC00
|
|
717
|
+
&& deltaArg.charCodeAt(0) <= 0xDFFF
|
|
718
|
+
) ? -2 : 0;
|
|
719
|
+
const result = textUtf8BytesArg + Buffer.byteLength(deltaArg, 'utf8') + boundaryCorrection;
|
|
720
|
+
return Number.isSafeInteger(result) && result >= 0 ? result : undefined;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
const fragmentControllerTextDelta = (valueArg: string): IControllerTextDeltaFragment[] => {
|
|
724
|
+
const fragments: IControllerTextDeltaFragment[] = [];
|
|
725
|
+
let delta = '';
|
|
726
|
+
let utf8Bytes = 0;
|
|
727
|
+
for (const codePoint of valueArg) {
|
|
728
|
+
const codePointBytes = Buffer.byteLength(codePoint, 'utf8');
|
|
729
|
+
if (
|
|
730
|
+
delta !== ''
|
|
731
|
+
&& utf8Bytes + codePointBytes > controllerLiveMessageDeltaTargetBytes
|
|
732
|
+
) {
|
|
733
|
+
fragments.push({ delta, utf8Bytes });
|
|
734
|
+
delta = '';
|
|
735
|
+
utf8Bytes = 0;
|
|
736
|
+
}
|
|
737
|
+
delta += codePoint;
|
|
738
|
+
utf8Bytes += codePointBytes;
|
|
739
|
+
}
|
|
740
|
+
if (delta !== '') fragments.push({ delta, utf8Bytes });
|
|
741
|
+
return fragments;
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const controllerMessageDeltaPendingKey = (eventArg: IControllerEvent): string | undefined => {
|
|
745
|
+
const delta = controllerEventMessageDelta(eventArg);
|
|
746
|
+
if (!delta) return undefined;
|
|
747
|
+
return JSON.stringify([
|
|
748
|
+
eventArg.type,
|
|
749
|
+
eventArg.harnessId ?? null,
|
|
750
|
+
eventArg.projectId ?? null,
|
|
751
|
+
controllerRuntimeIdKey(delta.sessionId),
|
|
752
|
+
controllerRuntimeIdKey(delta.partId),
|
|
753
|
+
]);
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const controllerMessageDeltaEventsAreContiguous = (
|
|
757
|
+
previousEventArg: IControllerEvent,
|
|
758
|
+
eventArg: IControllerEvent,
|
|
759
|
+
): boolean => {
|
|
760
|
+
const previous = controllerEventMessageDelta(previousEventArg);
|
|
761
|
+
const delta = controllerEventMessageDelta(eventArg);
|
|
762
|
+
if (!previous || !delta) return false;
|
|
763
|
+
return previousEventArg.type === eventArg.type
|
|
764
|
+
&& previousEventArg.harnessId === eventArg.harnessId
|
|
765
|
+
&& previousEventArg.projectId === eventArg.projectId
|
|
766
|
+
&& (previousEventArg.sessionId
|
|
767
|
+
? eventArg.sessionId !== undefined
|
|
768
|
+
&& controllerRuntimeIdKey(previousEventArg.sessionId)
|
|
769
|
+
=== controllerRuntimeIdKey(eventArg.sessionId)
|
|
770
|
+
: eventArg.sessionId === undefined)
|
|
771
|
+
&& controllerRuntimeIdKey(previous.sessionId) === controllerRuntimeIdKey(delta.sessionId)
|
|
772
|
+
&& controllerRuntimeIdKey(previous.messageId) === controllerRuntimeIdKey(delta.messageId)
|
|
773
|
+
&& controllerRuntimeIdKey(previous.partId) === controllerRuntimeIdKey(delta.partId)
|
|
774
|
+
&& previous.streamEpoch === delta.streamEpoch
|
|
775
|
+
&& previous.order?.messageIndex === delta.order?.messageIndex
|
|
776
|
+
&& previous.order?.partIndex === delta.order?.partIndex
|
|
777
|
+
&& previous.textUtf8Bytes === delta.baseTextUtf8Bytes
|
|
778
|
+
&& delta.revision > previous.revision;
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const mergeControllerMessageDeltaEvents = (
|
|
782
|
+
previousEventArg: IControllerEvent,
|
|
783
|
+
eventArg: IControllerEvent,
|
|
784
|
+
): IControllerEvent | undefined => {
|
|
785
|
+
if (!controllerMessageDeltaEventsAreContiguous(previousEventArg, eventArg)) return undefined;
|
|
786
|
+
const previous = controllerEventMessageDelta(previousEventArg)!;
|
|
787
|
+
const delta = controllerEventMessageDelta(eventArg)!;
|
|
788
|
+
const mergedDelta = {
|
|
789
|
+
...delta,
|
|
790
|
+
delta: `${previous.delta}${delta.delta}`,
|
|
791
|
+
baseTextUtf8Bytes: previous.baseTextUtf8Bytes,
|
|
792
|
+
};
|
|
793
|
+
return eventArg.reasoningDelta
|
|
794
|
+
? { ...eventArg, reasoningDelta: mergedDelta }
|
|
795
|
+
: { ...eventArg, textDelta: mergedDelta };
|
|
796
|
+
};
|
|
797
|
+
|
|
561
798
|
const markHistoryLimitedDigest = (digestArg: string): string => (
|
|
562
799
|
digestArg.startsWith(historyLimitedDigestPrefix)
|
|
563
800
|
? digestArg
|
|
@@ -1502,6 +1739,11 @@ export class OpenCodeController {
|
|
|
1502
1739
|
private resourceCoordinator?: ControllerResourceCoordinator;
|
|
1503
1740
|
private readonly browserViews = new Map<string, IControllerBrowserView>();
|
|
1504
1741
|
private readonly browserViewSlots = new Map<string, IControllerBrowserViewSlot>();
|
|
1742
|
+
private readonly browserViewRealtimeBucketsByPeer = new Map<string, ControllerBrowserTokenBucket>();
|
|
1743
|
+
private readonly browserViewRealtimeGlobalBucket = new ControllerBrowserTokenBucket(
|
|
1744
|
+
browserViewRealtimeGlobalRate,
|
|
1745
|
+
browserViewRealtimeGlobalBurst,
|
|
1746
|
+
);
|
|
1505
1747
|
private readonly authenticatedPeerIds = new Set<string>();
|
|
1506
1748
|
private openCodeResourceGeneration = 0;
|
|
1507
1749
|
private flexResourceGeneration = 0;
|
|
@@ -1950,7 +2192,7 @@ export class OpenCodeController {
|
|
|
1950
2192
|
headersTimeout: 10_000,
|
|
1951
2193
|
requestTimeout: 30_000,
|
|
1952
2194
|
virtualStreamAuthorizationAdapter: {
|
|
1953
|
-
bind: (authorizationArg, contextArg) => this.
|
|
2195
|
+
bind: (authorizationArg, contextArg) => this.bindBrowserStreamAuthorization(
|
|
1954
2196
|
authorizationArg,
|
|
1955
2197
|
contextArg,
|
|
1956
2198
|
),
|
|
@@ -2937,6 +3179,7 @@ export class OpenCodeController {
|
|
|
2937
3179
|
(terminalPeerLifecycle as ITerminalPeerLifecycleState).connected = false;
|
|
2938
3180
|
}
|
|
2939
3181
|
this.authenticatedPeerIds.delete(peerArg.id);
|
|
3182
|
+
this.browserViewRealtimeBucketsByPeer.delete(peerArg.id);
|
|
2940
3183
|
const cleanupResults = await Promise.allSettled([
|
|
2941
3184
|
this.terminalManager?.detachPeer(peerArg.id) ?? Promise.resolve(),
|
|
2942
3185
|
this.closeBrowserViewsForPeer(peerArg.id),
|
|
@@ -3134,6 +3377,7 @@ export class OpenCodeController {
|
|
|
3134
3377
|
}
|
|
3135
3378
|
this.resourceCoordinator = undefined;
|
|
3136
3379
|
this.authenticatedPeerIds.clear();
|
|
3380
|
+
this.browserViewRealtimeBucketsByPeer.clear();
|
|
3137
3381
|
|
|
3138
3382
|
await Promise.allSettled([
|
|
3139
3383
|
...(this.openCodeControllerEventDrainTask ? [this.openCodeControllerEventDrainTask] : []),
|
|
@@ -5699,43 +5943,52 @@ export class OpenCodeController {
|
|
|
5699
5943
|
'controller.browser.view.activate',
|
|
5700
5944
|
async (requestArg, toolsArg) => {
|
|
5701
5945
|
const peer = peerFromLocalData(toolsArg?.localData);
|
|
5702
|
-
const { viewId } = validateRequest(
|
|
5946
|
+
const { viewId, streamGeneration } = validateRequest(
|
|
5947
|
+
() => assertBrowserViewActivateRequest(requestArg),
|
|
5948
|
+
);
|
|
5703
5949
|
return this.runOperation(peer, async (signalArg) => {
|
|
5704
|
-
await this.activateBrowserView(viewId, peer
|
|
5950
|
+
await this.activateBrowserView(viewId, peer, streamGeneration, signalArg);
|
|
5705
5951
|
return { activated: true as const };
|
|
5706
5952
|
}, toolsArg?.abortSignal);
|
|
5707
5953
|
},
|
|
5708
5954
|
));
|
|
5709
5955
|
|
|
5956
|
+
protectedRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_ControllerBrowserViewRecover>(
|
|
5957
|
+
'controller.browser.view.recover',
|
|
5958
|
+
async (requestArg, toolsArg) => {
|
|
5959
|
+
const peer = peerFromLocalData(toolsArg?.localData);
|
|
5960
|
+
const transportConnection = this.requireTypedServer().getServerConnectionForRequest(toolsArg);
|
|
5961
|
+
const parsed = validateRequest(() => assertBrowserViewRecoverRequest(requestArg));
|
|
5962
|
+
return this.runOperation(peer, async (signalArg) => this.recoverBrowserView(
|
|
5963
|
+
peer,
|
|
5964
|
+
transportConnection,
|
|
5965
|
+
parsed,
|
|
5966
|
+
signalArg,
|
|
5967
|
+
), toolsArg?.abortSignal);
|
|
5968
|
+
},
|
|
5969
|
+
));
|
|
5970
|
+
|
|
5710
5971
|
protectedRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_ControllerBrowserViewClose>(
|
|
5711
5972
|
'controller.browser.view.close',
|
|
5712
5973
|
async (requestArg, toolsArg) => {
|
|
5713
5974
|
const peer = peerFromLocalData(toolsArg?.localData);
|
|
5714
|
-
const { viewId } = validateRequest(
|
|
5975
|
+
const { viewId, expectedStreamGeneration } = validateRequest(
|
|
5976
|
+
() => assertBrowserViewCloseRequest(requestArg),
|
|
5977
|
+
);
|
|
5715
5978
|
return this.runOperation(peer, async () => this.runAudited(
|
|
5716
5979
|
{ type: 'resource.browser.view.close', peerId: peer.id },
|
|
5717
5980
|
async () => {
|
|
5718
|
-
await this.closeBrowserView(
|
|
5719
|
-
|
|
5981
|
+
const closed = await this.closeBrowserView(
|
|
5982
|
+
viewId,
|
|
5983
|
+
peer,
|
|
5984
|
+
expectedStreamGeneration,
|
|
5985
|
+
);
|
|
5986
|
+
return { closed };
|
|
5720
5987
|
},
|
|
5721
5988
|
));
|
|
5722
5989
|
},
|
|
5723
5990
|
));
|
|
5724
5991
|
|
|
5725
|
-
protectedRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_ControllerBrowserViewOperate>(
|
|
5726
|
-
'controller.browser.view.operate',
|
|
5727
|
-
async (requestArg, toolsArg) => {
|
|
5728
|
-
const peer = peerFromLocalData(toolsArg?.localData);
|
|
5729
|
-
const parsed = validateRequest(() => assertBrowserViewOperateRequest(requestArg));
|
|
5730
|
-
return this.runOperation(peer, async (signalArg) => this.operateBrowserView(
|
|
5731
|
-
peer.id,
|
|
5732
|
-
parsed.viewId,
|
|
5733
|
-
parsed.operation,
|
|
5734
|
-
signalArg,
|
|
5735
|
-
), toolsArg?.abortSignal);
|
|
5736
|
-
},
|
|
5737
|
-
));
|
|
5738
|
-
|
|
5739
5992
|
protectedRouter.addTypedHandler(
|
|
5740
5993
|
new plugins.typedrequest.TypedHandler<IReq_ControllerSessionGroupsGet>(
|
|
5741
5994
|
'controller.sessiongroups.get',
|
|
@@ -9992,6 +10245,10 @@ export class OpenCodeController {
|
|
|
9992
10245
|
this.handleFlexTextUpdate(eventArg);
|
|
9993
10246
|
return;
|
|
9994
10247
|
}
|
|
10248
|
+
if (eventArg.type === 'part.delta') {
|
|
10249
|
+
this.handleFlexPartDelta(eventArg);
|
|
10250
|
+
return;
|
|
10251
|
+
}
|
|
9995
10252
|
if (eventArg.type === 'permissions.changed') {
|
|
9996
10253
|
if (this.autoAcceptPermissions || this.hasFlexYoloSessions()) {
|
|
9997
10254
|
this.queueFlexAutoAcceptSweep(eventArg.scopeId, eventArg.sessionId);
|
|
@@ -10413,7 +10670,12 @@ export class OpenCodeController {
|
|
|
10413
10670
|
const bytes = Buffer.byteLength(JSON.stringify(eventArg), 'utf8');
|
|
10414
10671
|
return {
|
|
10415
10672
|
tool: eventArg.toolExecution ? bytes : 0,
|
|
10416
|
-
message: eventArg.reasoningUpdate
|
|
10673
|
+
message: eventArg.reasoningUpdate
|
|
10674
|
+
|| eventArg.reasoningDelta
|
|
10675
|
+
|| eventArg.textUpdate
|
|
10676
|
+
|| eventArg.textDelta
|
|
10677
|
+
? bytes
|
|
10678
|
+
: 0,
|
|
10417
10679
|
};
|
|
10418
10680
|
}
|
|
10419
10681
|
|
|
@@ -10573,7 +10835,12 @@ export class OpenCodeController {
|
|
|
10573
10835
|
this.latestFlexTextUpdates.clear();
|
|
10574
10836
|
this.latestFlexMessageUpdateBytes = 0;
|
|
10575
10837
|
for (const [key, event] of this.pendingFlexControllerEvents) {
|
|
10576
|
-
if (
|
|
10838
|
+
if (
|
|
10839
|
+
event.reasoningUpdate
|
|
10840
|
+
|| event.reasoningDelta
|
|
10841
|
+
|| event.textUpdate
|
|
10842
|
+
|| event.textDelta
|
|
10843
|
+
) this.deletePendingFlexControllerEvent(key);
|
|
10577
10844
|
}
|
|
10578
10845
|
const timestamp = Date.now();
|
|
10579
10846
|
const barrier: IControllerEvent = {
|
|
@@ -10659,11 +10926,19 @@ export class OpenCodeController {
|
|
|
10659
10926
|
for (const [key, event] of this.pendingFlexControllerEvents) {
|
|
10660
10927
|
if (
|
|
10661
10928
|
event.projectId === projectIdArg
|
|
10662
|
-
&& (
|
|
10929
|
+
&& (
|
|
10930
|
+
event.toolExecution
|
|
10931
|
+
|| event.reasoningUpdate
|
|
10932
|
+
|| event.reasoningDelta
|
|
10933
|
+
|| event.textUpdate
|
|
10934
|
+
|| event.textDelta
|
|
10935
|
+
)
|
|
10663
10936
|
&& controllerRuntimeIdKey(
|
|
10664
10937
|
event.toolExecution?.sessionId
|
|
10665
10938
|
?? event.reasoningUpdate?.sessionId
|
|
10666
|
-
?? event.
|
|
10939
|
+
?? event.reasoningDelta?.sessionId
|
|
10940
|
+
?? event.textUpdate?.sessionId
|
|
10941
|
+
?? event.textDelta!.sessionId,
|
|
10667
10942
|
) === sessionKey
|
|
10668
10943
|
) this.deletePendingFlexControllerEvent(key);
|
|
10669
10944
|
}
|
|
@@ -10729,7 +11004,13 @@ export class OpenCodeController {
|
|
|
10729
11004
|
for (const [key, event] of this.pendingFlexControllerEvents) {
|
|
10730
11005
|
if (
|
|
10731
11006
|
event.projectId === projectIdArg
|
|
10732
|
-
&& (
|
|
11007
|
+
&& (
|
|
11008
|
+
event.toolExecution
|
|
11009
|
+
|| event.reasoningUpdate
|
|
11010
|
+
|| event.reasoningDelta
|
|
11011
|
+
|| event.textUpdate
|
|
11012
|
+
|| event.textDelta
|
|
11013
|
+
)
|
|
10733
11014
|
) {
|
|
10734
11015
|
this.deletePendingFlexControllerEvent(key);
|
|
10735
11016
|
}
|
|
@@ -10886,7 +11167,11 @@ export class OpenCodeController {
|
|
|
10886
11167
|
}
|
|
10887
11168
|
this.setLatestFlexReasoningUpdate(
|
|
10888
11169
|
key,
|
|
10889
|
-
{
|
|
11170
|
+
{
|
|
11171
|
+
projectId: eventArg.scopeId,
|
|
11172
|
+
update,
|
|
11173
|
+
textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
|
|
11174
|
+
},
|
|
10890
11175
|
eventBytes,
|
|
10891
11176
|
);
|
|
10892
11177
|
this.queueFlexControllerEvent(controllerEvent);
|
|
@@ -10946,12 +11231,169 @@ export class OpenCodeController {
|
|
|
10946
11231
|
}
|
|
10947
11232
|
this.setLatestFlexTextUpdate(
|
|
10948
11233
|
key,
|
|
10949
|
-
{
|
|
11234
|
+
{
|
|
11235
|
+
projectId: eventArg.scopeId,
|
|
11236
|
+
update,
|
|
11237
|
+
textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
|
|
11238
|
+
},
|
|
10950
11239
|
eventBytes,
|
|
10951
11240
|
);
|
|
10952
11241
|
this.queueFlexControllerEvent(controllerEvent);
|
|
10953
11242
|
}
|
|
10954
11243
|
|
|
11244
|
+
private handleFlexPartDelta(
|
|
11245
|
+
eventArg: Extract<TFlexChildEvent, { type: 'part.delta' }>,
|
|
11246
|
+
): void {
|
|
11247
|
+
if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
|
|
11248
|
+
const sourceUpdatedAt = Date.parse(eventArg.timestamp);
|
|
11249
|
+
if (!Number.isFinite(sourceUpdatedAt)) {
|
|
11250
|
+
this.advanceFlexMessageStreamEpoch(true);
|
|
11251
|
+
return;
|
|
11252
|
+
}
|
|
11253
|
+
const sessionId = flexRuntimeId(eventArg.sessionId);
|
|
11254
|
+
const messageId = flexRuntimeId(eventArg.messageId);
|
|
11255
|
+
const partId = flexRuntimeId(eventArg.partId);
|
|
11256
|
+
const order = { messageIndex: eventArg.messageIndex, partIndex: eventArg.partIndex };
|
|
11257
|
+
const key = this.openCodeToolExecutionKey(eventArg.scopeId, { sessionId, partId });
|
|
11258
|
+
const cache = eventArg.partType === 'reasoning'
|
|
11259
|
+
? this.latestFlexReasoningUpdates
|
|
11260
|
+
: this.latestFlexTextUpdates;
|
|
11261
|
+
const entry = cache.get(key);
|
|
11262
|
+
const previous = entry?.update;
|
|
11263
|
+
if (
|
|
11264
|
+
!entry
|
|
11265
|
+
|| !previous
|
|
11266
|
+
|| this.flexMessageStreamEpoch === 0
|
|
11267
|
+
|| entry.projectId !== eventArg.scopeId
|
|
11268
|
+
|| previous.status !== 'running'
|
|
11269
|
+
|| previous.streamEpoch !== this.flexMessageStreamEpoch
|
|
11270
|
+
|| previous.revision > this.flexMessageStreamRevision
|
|
11271
|
+
|| controllerRuntimeIdKey(previous.sessionId) !== controllerRuntimeIdKey(sessionId)
|
|
11272
|
+
|| controllerRuntimeIdKey(previous.messageId) !== controllerRuntimeIdKey(messageId)
|
|
11273
|
+
|| controllerRuntimeIdKey(previous.partId) !== controllerRuntimeIdKey(partId)
|
|
11274
|
+
|| previous.order?.messageIndex !== order.messageIndex
|
|
11275
|
+
|| previous.order.partIndex !== order.partIndex
|
|
11276
|
+
|| sourceUpdatedAt < previous.sourceUpdatedAt
|
|
11277
|
+
|| entry.textUtf8Bytes !== eventArg.baseTextUtf8Bytes
|
|
11278
|
+
) {
|
|
11279
|
+
this.advanceFlexMessageStreamEpoch(true);
|
|
11280
|
+
return;
|
|
11281
|
+
}
|
|
11282
|
+
const textUtf8Bytes = controllerTextAfterAppendUtf8Bytes(
|
|
11283
|
+
previous.text,
|
|
11284
|
+
entry.textUtf8Bytes,
|
|
11285
|
+
eventArg.delta,
|
|
11286
|
+
);
|
|
11287
|
+
if (
|
|
11288
|
+
textUtf8Bytes === undefined
|
|
11289
|
+
|| textUtf8Bytes <= entry.textUtf8Bytes
|
|
11290
|
+
|| textUtf8Bytes !== eventArg.textUtf8Bytes
|
|
11291
|
+
) {
|
|
11292
|
+
this.advanceFlexMessageStreamEpoch(true);
|
|
11293
|
+
return;
|
|
11294
|
+
}
|
|
11295
|
+
|
|
11296
|
+
const revision = this.flexMessageStreamRevision + 1;
|
|
11297
|
+
const update = {
|
|
11298
|
+
...previous,
|
|
11299
|
+
text: `${previous.text}${eventArg.delta}`,
|
|
11300
|
+
sourceUpdatedAt,
|
|
11301
|
+
revision,
|
|
11302
|
+
streamEpoch: this.flexMessageStreamEpoch,
|
|
11303
|
+
} as IControllerReasoningUpdate | IControllerTextUpdate;
|
|
11304
|
+
const controllerEvent: IControllerEvent = eventArg.partType === 'reasoning'
|
|
11305
|
+
? {
|
|
11306
|
+
type: 'session.reasoning.delta',
|
|
11307
|
+
harnessId: 'flex',
|
|
11308
|
+
projectId: eventArg.scopeId,
|
|
11309
|
+
sessionId,
|
|
11310
|
+
reasoningDelta: {
|
|
11311
|
+
sessionId,
|
|
11312
|
+
messageId,
|
|
11313
|
+
partId,
|
|
11314
|
+
delta: eventArg.delta,
|
|
11315
|
+
baseTextUtf8Bytes: eventArg.baseTextUtf8Bytes,
|
|
11316
|
+
textUtf8Bytes,
|
|
11317
|
+
order,
|
|
11318
|
+
sourceUpdatedAt,
|
|
11319
|
+
revision,
|
|
11320
|
+
streamEpoch: this.flexMessageStreamEpoch,
|
|
11321
|
+
},
|
|
11322
|
+
timestamp: sourceUpdatedAt,
|
|
11323
|
+
}
|
|
11324
|
+
: {
|
|
11325
|
+
type: 'session.text.delta',
|
|
11326
|
+
harnessId: 'flex',
|
|
11327
|
+
projectId: eventArg.scopeId,
|
|
11328
|
+
sessionId,
|
|
11329
|
+
textDelta: {
|
|
11330
|
+
sessionId,
|
|
11331
|
+
messageId,
|
|
11332
|
+
partId,
|
|
11333
|
+
delta: eventArg.delta,
|
|
11334
|
+
baseTextUtf8Bytes: eventArg.baseTextUtf8Bytes,
|
|
11335
|
+
textUtf8Bytes,
|
|
11336
|
+
order,
|
|
11337
|
+
sourceUpdatedAt,
|
|
11338
|
+
revision,
|
|
11339
|
+
streamEpoch: this.flexMessageStreamEpoch,
|
|
11340
|
+
},
|
|
11341
|
+
timestamp: sourceUpdatedAt,
|
|
11342
|
+
};
|
|
11343
|
+
const snapshotEvent: IControllerEvent = eventArg.partType === 'reasoning'
|
|
11344
|
+
? {
|
|
11345
|
+
type: 'session.reasoning.updated',
|
|
11346
|
+
harnessId: 'flex',
|
|
11347
|
+
projectId: eventArg.scopeId,
|
|
11348
|
+
sessionId,
|
|
11349
|
+
reasoningUpdate: update as IControllerReasoningUpdate,
|
|
11350
|
+
timestamp: sourceUpdatedAt,
|
|
11351
|
+
}
|
|
11352
|
+
: {
|
|
11353
|
+
type: 'session.text.updated',
|
|
11354
|
+
harnessId: 'flex',
|
|
11355
|
+
projectId: eventArg.scopeId,
|
|
11356
|
+
sessionId,
|
|
11357
|
+
textUpdate: update as IControllerTextUpdate,
|
|
11358
|
+
timestamp: sourceUpdatedAt,
|
|
11359
|
+
};
|
|
11360
|
+
const eventBytes = Buffer.byteLength(JSON.stringify(controllerEvent), 'utf8');
|
|
11361
|
+
const snapshotBytes = Buffer.byteLength(JSON.stringify(snapshotEvent), 'utf8');
|
|
11362
|
+
if (
|
|
11363
|
+
eventBytes > controllerMaxLiveMessageEventBytes
|
|
11364
|
+
|| snapshotBytes > controllerMaxLiveMessageEventBytes
|
|
11365
|
+
|| this.latestFlexMessageUpdateBytes - (entry.bytes ?? 0) + snapshotBytes
|
|
11366
|
+
> maxLatestFlexMessageUpdateBytes
|
|
11367
|
+
) {
|
|
11368
|
+
this.advanceFlexMessageStreamEpoch(true);
|
|
11369
|
+
return;
|
|
11370
|
+
}
|
|
11371
|
+
|
|
11372
|
+
this.flexMessageStreamRevision = revision;
|
|
11373
|
+
if (eventArg.partType === 'reasoning') {
|
|
11374
|
+
this.setLatestFlexReasoningUpdate(
|
|
11375
|
+
key,
|
|
11376
|
+
{
|
|
11377
|
+
projectId: eventArg.scopeId,
|
|
11378
|
+
update: update as IControllerReasoningUpdate,
|
|
11379
|
+
textUtf8Bytes,
|
|
11380
|
+
},
|
|
11381
|
+
snapshotBytes,
|
|
11382
|
+
);
|
|
11383
|
+
} else {
|
|
11384
|
+
this.setLatestFlexTextUpdate(
|
|
11385
|
+
key,
|
|
11386
|
+
{
|
|
11387
|
+
projectId: eventArg.scopeId,
|
|
11388
|
+
update: update as IControllerTextUpdate,
|
|
11389
|
+
textUtf8Bytes,
|
|
11390
|
+
},
|
|
11391
|
+
snapshotBytes,
|
|
11392
|
+
);
|
|
11393
|
+
}
|
|
11394
|
+
this.queueFlexControllerEvent(controllerEvent);
|
|
11395
|
+
}
|
|
11396
|
+
|
|
10955
11397
|
private getOpenCodeToolStreamCursor(): IControllerToolStreamCursor {
|
|
10956
11398
|
return {
|
|
10957
11399
|
streamEpoch: this.openCodeToolStreamEpoch,
|
|
@@ -11075,12 +11517,25 @@ export class OpenCodeController {
|
|
|
11075
11517
|
|
|
11076
11518
|
private advanceOpenCodeMessageStreamEpoch(includeSessionsChangedArg: boolean): void {
|
|
11077
11519
|
if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
|
|
11520
|
+
const pendingEntries = includeSessionsChangedArg ? 2 : 1;
|
|
11521
|
+
if (
|
|
11522
|
+
this.pendingOpenCodeControllerEvents.size
|
|
11523
|
+
> maxPendingOpenCodeControllerEvents - pendingEntries
|
|
11524
|
+
) {
|
|
11525
|
+
this.advanceOpenCodeStreams(includeSessionsChangedArg);
|
|
11526
|
+
return;
|
|
11527
|
+
}
|
|
11078
11528
|
this.openCodeMessageStreamEpoch += 1;
|
|
11079
11529
|
this.openCodeMessageStreamRevision = 0;
|
|
11080
11530
|
this.latestOpenCodeReasoningUpdates.clear();
|
|
11081
11531
|
this.latestOpenCodeTextUpdates.clear();
|
|
11082
11532
|
for (const [key, event] of this.pendingOpenCodeControllerEvents) {
|
|
11083
|
-
if (
|
|
11533
|
+
if (
|
|
11534
|
+
event.reasoningUpdate
|
|
11535
|
+
|| event.reasoningDelta
|
|
11536
|
+
|| event.textUpdate
|
|
11537
|
+
|| event.textDelta
|
|
11538
|
+
) this.pendingOpenCodeControllerEvents.delete(key);
|
|
11084
11539
|
}
|
|
11085
11540
|
const timestamp = Date.now();
|
|
11086
11541
|
const barrier: IControllerEvent = {
|
|
@@ -11167,6 +11622,143 @@ export class OpenCodeController {
|
|
|
11167
11622
|
if (!cache.has(key) && cache.size >= maxLatestOpenCodeMessageUpdates) {
|
|
11168
11623
|
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11169
11624
|
}
|
|
11625
|
+
const previousEntry = cache.get(key);
|
|
11626
|
+
const previous = previousEntry?.update;
|
|
11627
|
+
if (
|
|
11628
|
+
previousEntry !== undefined
|
|
11629
|
+
&& previous !== undefined
|
|
11630
|
+
&& previousEntry.projectId === projectId
|
|
11631
|
+
&& previous.streamEpoch === this.openCodeMessageStreamEpoch
|
|
11632
|
+
&& previous.revision <= this.openCodeMessageStreamRevision
|
|
11633
|
+
&& controllerRuntimeIdKey(previous.sessionId) === controllerRuntimeIdKey(updateArg.sessionId)
|
|
11634
|
+
&& controllerRuntimeIdKey(previous.messageId) === controllerRuntimeIdKey(updateArg.messageId)
|
|
11635
|
+
&& controllerRuntimeIdKey(previous.partId) === controllerRuntimeIdKey(updateArg.partId)
|
|
11636
|
+
&& previous.status === 'running'
|
|
11637
|
+
&& updateArg.status === 'running'
|
|
11638
|
+
&& updateArg.text.length > previous.text.length
|
|
11639
|
+
&& updateArg.text.startsWith(previous.text)
|
|
11640
|
+
&& Number.isSafeInteger(previousEntry.textUtf8Bytes)
|
|
11641
|
+
) {
|
|
11642
|
+
const deltaText = updateArg.text.slice(previous.text.length);
|
|
11643
|
+
const fragments = fragmentControllerTextDelta(deltaText);
|
|
11644
|
+
const textUtf8Bytes = controllerTextAfterAppendUtf8Bytes(
|
|
11645
|
+
previous.text,
|
|
11646
|
+
previousEntry.textUtf8Bytes,
|
|
11647
|
+
deltaText,
|
|
11648
|
+
);
|
|
11649
|
+
const finalRevision = this.openCodeMessageStreamRevision + fragments.length;
|
|
11650
|
+
if (
|
|
11651
|
+
fragments.length === 0
|
|
11652
|
+
|| textUtf8Bytes === undefined
|
|
11653
|
+
|| textUtf8Bytes <= previousEntry.textUtf8Bytes
|
|
11654
|
+
|| !Number.isSafeInteger(finalRevision)
|
|
11655
|
+
) {
|
|
11656
|
+
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11657
|
+
return;
|
|
11658
|
+
}
|
|
11659
|
+
const update = {
|
|
11660
|
+
...updateArg,
|
|
11661
|
+
revision: finalRevision,
|
|
11662
|
+
streamEpoch: this.openCodeMessageStreamEpoch,
|
|
11663
|
+
} as IControllerReasoningUpdate | IControllerTextUpdate;
|
|
11664
|
+
const snapshotEvent: IControllerEvent = typeArg === 'reasoning'
|
|
11665
|
+
? {
|
|
11666
|
+
type: 'session.reasoning.updated',
|
|
11667
|
+
harnessId: 'opencode',
|
|
11668
|
+
projectId,
|
|
11669
|
+
sessionId: update.sessionId,
|
|
11670
|
+
reasoningUpdate: update as IControllerReasoningUpdate,
|
|
11671
|
+
timestamp: update.sourceUpdatedAt,
|
|
11672
|
+
}
|
|
11673
|
+
: {
|
|
11674
|
+
type: 'session.text.updated',
|
|
11675
|
+
harnessId: 'opencode',
|
|
11676
|
+
projectId,
|
|
11677
|
+
sessionId: update.sessionId,
|
|
11678
|
+
textUpdate: update as IControllerTextUpdate,
|
|
11679
|
+
timestamp: update.sourceUpdatedAt,
|
|
11680
|
+
};
|
|
11681
|
+
const snapshotBytes = Buffer.byteLength(JSON.stringify(snapshotEvent), 'utf8');
|
|
11682
|
+
if (snapshotBytes > controllerMaxLiveMessageEventBytes) {
|
|
11683
|
+
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11684
|
+
return;
|
|
11685
|
+
}
|
|
11686
|
+
|
|
11687
|
+
const events: IControllerEvent[] = [];
|
|
11688
|
+
let baseTextUtf8Bytes = previousEntry.textUtf8Bytes;
|
|
11689
|
+
for (const [index, fragment] of fragments.entries()) {
|
|
11690
|
+
const fragmentTextUtf8Bytes = index === 0
|
|
11691
|
+
? controllerTextAfterAppendUtf8Bytes(
|
|
11692
|
+
previous.text,
|
|
11693
|
+
baseTextUtf8Bytes,
|
|
11694
|
+
fragment.delta,
|
|
11695
|
+
)
|
|
11696
|
+
: baseTextUtf8Bytes + fragment.utf8Bytes;
|
|
11697
|
+
if (
|
|
11698
|
+
fragmentTextUtf8Bytes === undefined
|
|
11699
|
+
|| !Number.isSafeInteger(fragmentTextUtf8Bytes)
|
|
11700
|
+
|| fragmentTextUtf8Bytes <= baseTextUtf8Bytes
|
|
11701
|
+
) {
|
|
11702
|
+
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11703
|
+
return;
|
|
11704
|
+
}
|
|
11705
|
+
const revision = this.openCodeMessageStreamRevision + index + 1;
|
|
11706
|
+
const commonDelta = {
|
|
11707
|
+
sessionId: update.sessionId,
|
|
11708
|
+
messageId: update.messageId,
|
|
11709
|
+
partId: update.partId,
|
|
11710
|
+
delta: fragment.delta,
|
|
11711
|
+
baseTextUtf8Bytes,
|
|
11712
|
+
textUtf8Bytes: fragmentTextUtf8Bytes,
|
|
11713
|
+
sourceUpdatedAt: update.sourceUpdatedAt,
|
|
11714
|
+
revision,
|
|
11715
|
+
streamEpoch: this.openCodeMessageStreamEpoch,
|
|
11716
|
+
};
|
|
11717
|
+
const event: IControllerEvent = typeArg === 'reasoning'
|
|
11718
|
+
? {
|
|
11719
|
+
type: 'session.reasoning.delta',
|
|
11720
|
+
harnessId: 'opencode',
|
|
11721
|
+
projectId,
|
|
11722
|
+
sessionId: update.sessionId,
|
|
11723
|
+
reasoningDelta: commonDelta,
|
|
11724
|
+
timestamp: update.sourceUpdatedAt,
|
|
11725
|
+
}
|
|
11726
|
+
: {
|
|
11727
|
+
type: 'session.text.delta',
|
|
11728
|
+
harnessId: 'opencode',
|
|
11729
|
+
projectId,
|
|
11730
|
+
sessionId: update.sessionId,
|
|
11731
|
+
textDelta: commonDelta,
|
|
11732
|
+
timestamp: update.sourceUpdatedAt,
|
|
11733
|
+
};
|
|
11734
|
+
if (Buffer.byteLength(JSON.stringify(event), 'utf8') > controllerMaxLiveMessageEventBytes) {
|
|
11735
|
+
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11736
|
+
return;
|
|
11737
|
+
}
|
|
11738
|
+
events.push(event);
|
|
11739
|
+
baseTextUtf8Bytes = fragmentTextUtf8Bytes;
|
|
11740
|
+
}
|
|
11741
|
+
if (baseTextUtf8Bytes !== textUtf8Bytes) {
|
|
11742
|
+
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11743
|
+
return;
|
|
11744
|
+
}
|
|
11745
|
+
|
|
11746
|
+
this.openCodeMessageStreamRevision = finalRevision;
|
|
11747
|
+
cache.delete(key);
|
|
11748
|
+
cache.set(key, {
|
|
11749
|
+
projectId,
|
|
11750
|
+
update,
|
|
11751
|
+
textUtf8Bytes,
|
|
11752
|
+
bytes: snapshotBytes,
|
|
11753
|
+
} as ILatestReasoningUpdate & ILatestTextUpdate);
|
|
11754
|
+
const streamEpoch = this.openCodeMessageStreamEpoch;
|
|
11755
|
+
for (const event of events) {
|
|
11756
|
+
this.queueOpenCodeControllerEvent(event);
|
|
11757
|
+
if (this.openCodeMessageStreamEpoch !== streamEpoch) return;
|
|
11758
|
+
}
|
|
11759
|
+
return;
|
|
11760
|
+
}
|
|
11761
|
+
|
|
11170
11762
|
this.openCodeMessageStreamRevision += 1;
|
|
11171
11763
|
const update = {
|
|
11172
11764
|
...updateArg,
|
|
@@ -11190,12 +11782,18 @@ export class OpenCodeController {
|
|
|
11190
11782
|
textUpdate: update as IControllerTextUpdate,
|
|
11191
11783
|
timestamp: update.sourceUpdatedAt,
|
|
11192
11784
|
};
|
|
11193
|
-
|
|
11785
|
+
const eventBytes = Buffer.byteLength(JSON.stringify(event), 'utf8');
|
|
11786
|
+
if (eventBytes > controllerMaxLiveMessageEventBytes) {
|
|
11194
11787
|
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11195
11788
|
return;
|
|
11196
11789
|
}
|
|
11197
11790
|
cache.delete(key);
|
|
11198
|
-
cache.set(key, {
|
|
11791
|
+
cache.set(key, {
|
|
11792
|
+
projectId,
|
|
11793
|
+
update,
|
|
11794
|
+
textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
|
|
11795
|
+
bytes: eventBytes,
|
|
11796
|
+
} as ILatestReasoningUpdate & ILatestTextUpdate);
|
|
11199
11797
|
this.queueOpenCodeControllerEvent(event);
|
|
11200
11798
|
}
|
|
11201
11799
|
|
|
@@ -11242,6 +11840,7 @@ export class OpenCodeController {
|
|
|
11242
11840
|
}
|
|
11243
11841
|
|
|
11244
11842
|
private controllerEventKey(eventArg: IControllerEvent): string {
|
|
11843
|
+
const delta = controllerEventMessageDelta(eventArg);
|
|
11245
11844
|
return JSON.stringify([
|
|
11246
11845
|
eventArg.type,
|
|
11247
11846
|
eventArg.harnessId ?? null,
|
|
@@ -11256,6 +11855,18 @@ export class OpenCodeController {
|
|
|
11256
11855
|
eventArg.textUpdate
|
|
11257
11856
|
? controllerRuntimeIdKey(eventArg.textUpdate.partId)
|
|
11258
11857
|
: null,
|
|
11858
|
+
delta
|
|
11859
|
+
? [
|
|
11860
|
+
controllerRuntimeIdKey(delta.messageId),
|
|
11861
|
+
controllerRuntimeIdKey(delta.partId),
|
|
11862
|
+
delta.streamEpoch,
|
|
11863
|
+
delta.order?.messageIndex ?? null,
|
|
11864
|
+
delta.order?.partIndex ?? null,
|
|
11865
|
+
delta.baseTextUtf8Bytes,
|
|
11866
|
+
delta.textUtf8Bytes,
|
|
11867
|
+
delta.revision,
|
|
11868
|
+
]
|
|
11869
|
+
: null,
|
|
11259
11870
|
eventArg.toolExecution
|
|
11260
11871
|
? eventArg.toolExecution.status === 'completed' || eventArg.toolExecution.status === 'error'
|
|
11261
11872
|
? 'terminal'
|
|
@@ -11272,15 +11883,59 @@ export class OpenCodeController {
|
|
|
11272
11883
|
]);
|
|
11273
11884
|
}
|
|
11274
11885
|
|
|
11275
|
-
private
|
|
11276
|
-
|
|
11886
|
+
private latestPendingControllerDelta(
|
|
11887
|
+
queueArg: Map<string, IControllerEvent>,
|
|
11888
|
+
eventArg: IControllerEvent,
|
|
11889
|
+
): { key: string; event: IControllerEvent } | undefined {
|
|
11890
|
+
const pendingKey = controllerMessageDeltaPendingKey(eventArg);
|
|
11891
|
+
if (!pendingKey) return undefined;
|
|
11892
|
+
let tail: [string, IControllerEvent] | undefined;
|
|
11893
|
+
for (const entry of queueArg) tail = entry;
|
|
11894
|
+
if (!tail || controllerMessageDeltaPendingKey(tail[1]) !== pendingKey) return undefined;
|
|
11895
|
+
return { key: tail[0], event: tail[1] };
|
|
11896
|
+
}
|
|
11897
|
+
|
|
11898
|
+
private queueOpenCodeControllerEvent(eventArg: IControllerEvent): boolean {
|
|
11899
|
+
if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return false;
|
|
11900
|
+
const delta = controllerEventMessageDelta(eventArg);
|
|
11901
|
+
if (delta) {
|
|
11902
|
+
if (Buffer.byteLength(JSON.stringify(eventArg), 'utf8') > controllerMaxLiveMessageEventBytes) {
|
|
11903
|
+
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11904
|
+
return false;
|
|
11905
|
+
}
|
|
11906
|
+
const pending = this.latestPendingControllerDelta(
|
|
11907
|
+
this.pendingOpenCodeControllerEvents,
|
|
11908
|
+
eventArg,
|
|
11909
|
+
);
|
|
11910
|
+
if (pending) {
|
|
11911
|
+
const merged = mergeControllerMessageDeltaEvents(pending.event, eventArg);
|
|
11912
|
+
if (!merged) {
|
|
11913
|
+
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11914
|
+
return false;
|
|
11915
|
+
}
|
|
11916
|
+
if (
|
|
11917
|
+
Buffer.byteLength(JSON.stringify(merged), 'utf8')
|
|
11918
|
+
<= controllerMaxLiveMessageEventBytes
|
|
11919
|
+
) {
|
|
11920
|
+
this.pendingOpenCodeControllerEvents.set(pending.key, merged);
|
|
11921
|
+
this.startOpenCodeControllerEventDrain();
|
|
11922
|
+
return true;
|
|
11923
|
+
}
|
|
11924
|
+
}
|
|
11925
|
+
}
|
|
11277
11926
|
const key = this.controllerEventKey(eventArg);
|
|
11278
|
-
if (this.pendingOpenCodeControllerEvents.has(key)) {
|
|
11927
|
+
if (delta && this.pendingOpenCodeControllerEvents.has(key)) {
|
|
11928
|
+
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11929
|
+
return false;
|
|
11930
|
+
}
|
|
11931
|
+
if (!delta && this.pendingOpenCodeControllerEvents.has(key)) {
|
|
11279
11932
|
this.pendingOpenCodeControllerEvents.delete(key);
|
|
11280
11933
|
} else if (
|
|
11281
11934
|
this.pendingOpenCodeControllerEvents.size >= maxPendingOpenCodeControllerEvents
|
|
11282
11935
|
) {
|
|
11283
|
-
if (
|
|
11936
|
+
if (delta) {
|
|
11937
|
+
this.advanceOpenCodeStreams(true);
|
|
11938
|
+
} else if (eventArg.reasoningUpdate || eventArg.textUpdate) {
|
|
11284
11939
|
this.advanceOpenCodeMessageStreamEpoch(true);
|
|
11285
11940
|
} else if (eventArg.toolExecution) {
|
|
11286
11941
|
this.advanceOpenCodeToolStreamEpoch(true);
|
|
@@ -11312,10 +11967,11 @@ export class OpenCodeController {
|
|
|
11312
11967
|
rebasedEvent,
|
|
11313
11968
|
);
|
|
11314
11969
|
}
|
|
11315
|
-
return;
|
|
11970
|
+
return false;
|
|
11316
11971
|
}
|
|
11317
11972
|
this.pendingOpenCodeControllerEvents.set(key, eventArg);
|
|
11318
11973
|
this.startOpenCodeControllerEventDrain();
|
|
11974
|
+
return true;
|
|
11319
11975
|
}
|
|
11320
11976
|
|
|
11321
11977
|
private startOpenCodeControllerEventDrain(): void {
|
|
@@ -11382,7 +12038,11 @@ export class OpenCodeController {
|
|
|
11382
12038
|
};
|
|
11383
12039
|
this.setLatestFlexReasoningUpdate(
|
|
11384
12040
|
this.openCodeToolExecutionKey(eventArg.projectId, update),
|
|
11385
|
-
{
|
|
12041
|
+
{
|
|
12042
|
+
projectId: eventArg.projectId,
|
|
12043
|
+
update,
|
|
12044
|
+
textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
|
|
12045
|
+
},
|
|
11386
12046
|
Buffer.byteLength(JSON.stringify(event), 'utf8'),
|
|
11387
12047
|
);
|
|
11388
12048
|
return event;
|
|
@@ -11401,7 +12061,11 @@ export class OpenCodeController {
|
|
|
11401
12061
|
};
|
|
11402
12062
|
this.setLatestFlexTextUpdate(
|
|
11403
12063
|
this.openCodeToolExecutionKey(eventArg.projectId, update),
|
|
11404
|
-
{
|
|
12064
|
+
{
|
|
12065
|
+
projectId: eventArg.projectId,
|
|
12066
|
+
update,
|
|
12067
|
+
textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
|
|
12068
|
+
},
|
|
11405
12069
|
Buffer.byteLength(JSON.stringify(event), 'utf8'),
|
|
11406
12070
|
);
|
|
11407
12071
|
return event;
|
|
@@ -11409,10 +12073,51 @@ export class OpenCodeController {
|
|
|
11409
12073
|
return eventArg;
|
|
11410
12074
|
}
|
|
11411
12075
|
|
|
11412
|
-
private queueFlexControllerEvent(eventArg: IControllerEvent):
|
|
11413
|
-
if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
|
|
12076
|
+
private queueFlexControllerEvent(eventArg: IControllerEvent): boolean {
|
|
12077
|
+
if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return false;
|
|
12078
|
+
const delta = controllerEventMessageDelta(eventArg);
|
|
12079
|
+
if (delta) {
|
|
12080
|
+
if (Buffer.byteLength(JSON.stringify(eventArg), 'utf8') > controllerMaxLiveMessageEventBytes) {
|
|
12081
|
+
this.advanceFlexMessageStreamEpoch(true);
|
|
12082
|
+
return false;
|
|
12083
|
+
}
|
|
12084
|
+
const pending = this.latestPendingControllerDelta(
|
|
12085
|
+
this.pendingFlexControllerEvents,
|
|
12086
|
+
eventArg,
|
|
12087
|
+
);
|
|
12088
|
+
if (pending) {
|
|
12089
|
+
const merged = mergeControllerMessageDeltaEvents(pending.event, eventArg);
|
|
12090
|
+
if (!merged) {
|
|
12091
|
+
this.advanceFlexMessageStreamEpoch(true);
|
|
12092
|
+
return false;
|
|
12093
|
+
}
|
|
12094
|
+
const mergedBytes = this.flexControllerEventLiveBytes(merged);
|
|
12095
|
+
if (
|
|
12096
|
+
mergedBytes.message <= controllerMaxLiveMessageEventBytes
|
|
12097
|
+
) {
|
|
12098
|
+
const pendingBytes = this.flexControllerEventLiveBytes(pending.event);
|
|
12099
|
+
if (
|
|
12100
|
+
this.latestFlexMessageUpdateBytes
|
|
12101
|
+
+ this.pendingFlexMessageEventBytes
|
|
12102
|
+
- pendingBytes.message
|
|
12103
|
+
+ mergedBytes.message
|
|
12104
|
+
> maxLatestFlexMessageUpdateBytes
|
|
12105
|
+
) {
|
|
12106
|
+
this.advanceFlexMessageStreamEpoch(true);
|
|
12107
|
+
return false;
|
|
12108
|
+
}
|
|
12109
|
+
this.setPendingFlexControllerEvent(pending.key, merged);
|
|
12110
|
+
this.startFlexControllerEventDrain();
|
|
12111
|
+
return true;
|
|
12112
|
+
}
|
|
12113
|
+
}
|
|
12114
|
+
}
|
|
11414
12115
|
const key = this.controllerEventKey(eventArg);
|
|
11415
|
-
this.
|
|
12116
|
+
if (delta && this.pendingFlexControllerEvents.has(key)) {
|
|
12117
|
+
this.advanceFlexMessageStreamEpoch(true);
|
|
12118
|
+
return false;
|
|
12119
|
+
}
|
|
12120
|
+
if (!delta) this.deletePendingFlexControllerEvent(key);
|
|
11416
12121
|
const liveBytes = this.flexControllerEventLiveBytes(eventArg);
|
|
11417
12122
|
const exceedsCountBound = this.pendingFlexControllerEvents.size >= maxPendingFlexControllerEvents;
|
|
11418
12123
|
const exceedsPendingBound = exceedsCountBound
|
|
@@ -11427,7 +12132,12 @@ export class OpenCodeController {
|
|
|
11427
12132
|
|| this.pendingFlexControllerEvents.size > maxPendingFlexControllerEvents - 3;
|
|
11428
12133
|
if (requiresCountCollapse) {
|
|
11429
12134
|
this.advanceFlexStreams(true);
|
|
11430
|
-
} else if (
|
|
12135
|
+
} else if (
|
|
12136
|
+
eventArg.reasoningUpdate
|
|
12137
|
+
|| eventArg.reasoningDelta
|
|
12138
|
+
|| eventArg.textUpdate
|
|
12139
|
+
|| eventArg.textDelta
|
|
12140
|
+
) {
|
|
11431
12141
|
this.advanceFlexMessageStreamEpoch(true);
|
|
11432
12142
|
} else if (eventArg.toolExecution) {
|
|
11433
12143
|
this.advanceFlexToolStreamEpoch(true);
|
|
@@ -11445,10 +12155,11 @@ export class OpenCodeController {
|
|
|
11445
12155
|
this.setPendingFlexControllerEvent(this.controllerEventKey(rebasedEvent), rebasedEvent);
|
|
11446
12156
|
}
|
|
11447
12157
|
this.startFlexControllerEventDrain();
|
|
11448
|
-
return;
|
|
12158
|
+
return false;
|
|
11449
12159
|
}
|
|
11450
12160
|
this.setPendingFlexControllerEvent(key, eventArg);
|
|
11451
12161
|
this.startFlexControllerEventDrain();
|
|
12162
|
+
return true;
|
|
11452
12163
|
}
|
|
11453
12164
|
|
|
11454
12165
|
private startFlexControllerEventDrain(): void {
|
|
@@ -12451,7 +13162,9 @@ export class OpenCodeController {
|
|
|
12451
13162
|
transportConnectionArg: plugins.typedsocket.ISmartServeConnectionWrapper,
|
|
12452
13163
|
requestArg: IReq_ControllerBrowserViewOpen['request'],
|
|
12453
13164
|
signalArg: AbortSignal,
|
|
12454
|
-
): Promise<
|
|
13165
|
+
): Promise<plugins.typedrequest.TReverseVirtualStreamDirections<
|
|
13166
|
+
IReq_ControllerBrowserViewOpen['response']
|
|
13167
|
+
>> {
|
|
12455
13168
|
return this.runBrowserViewSlotOperation(peerArg.id, async (slotArg) => {
|
|
12456
13169
|
const transportPeer = transportConnectionArg.peer;
|
|
12457
13170
|
signalArg.throwIfAborted();
|
|
@@ -12508,17 +13221,17 @@ export class OpenCodeController {
|
|
|
12508
13221
|
view = {
|
|
12509
13222
|
id: viewId,
|
|
12510
13223
|
peerId: peerArg.id,
|
|
13224
|
+
credentialId,
|
|
12511
13225
|
transportConnection: transportConnectionArg,
|
|
12512
13226
|
transportPeer,
|
|
12513
13227
|
resource,
|
|
12514
13228
|
handle,
|
|
12515
13229
|
authority: handle.lease.getAuthority(),
|
|
12516
|
-
|
|
13230
|
+
lifecycle: 'inactive',
|
|
12517
13231
|
activationAbortController: new AbortController(),
|
|
12518
|
-
|
|
12519
|
-
|
|
12520
|
-
|
|
12521
|
-
activeFrameStreams: new Set(),
|
|
13232
|
+
streamGeneration: 1,
|
|
13233
|
+
stateRevision: 0,
|
|
13234
|
+
recoveryAttempts: 0,
|
|
12522
13235
|
closing: false,
|
|
12523
13236
|
};
|
|
12524
13237
|
slotArg.view = view;
|
|
@@ -12534,6 +13247,9 @@ export class OpenCodeController {
|
|
|
12534
13247
|
);
|
|
12535
13248
|
}
|
|
12536
13249
|
this.browserViews.set(view.id, view);
|
|
13250
|
+
const transport = await this.createBrowserViewTransport(view, 1);
|
|
13251
|
+
this.beginBrowserViewTransportOpening(view, transport);
|
|
13252
|
+
signalArg.throwIfAborted();
|
|
12537
13253
|
view.activationTimer = setTimeout(() => {
|
|
12538
13254
|
this.scheduleBrowserViewClose(view!, new Error('Browser view activation timed out.'));
|
|
12539
13255
|
}, browserViewActivationTimeoutMs);
|
|
@@ -12542,7 +13258,10 @@ export class OpenCodeController {
|
|
|
12542
13258
|
viewId,
|
|
12543
13259
|
resourceId: resource.id,
|
|
12544
13260
|
attachmentRevision: resource.attachmentRevision,
|
|
12545
|
-
|
|
13261
|
+
streamGeneration: transport.generation,
|
|
13262
|
+
events: transport.events,
|
|
13263
|
+
operations: transport.operations,
|
|
13264
|
+
state: this.nextControllerBrowserViewState(view, handle.lease.getState()),
|
|
12546
13265
|
};
|
|
12547
13266
|
} catch (errorArg) {
|
|
12548
13267
|
let cleanupError: unknown;
|
|
@@ -12568,43 +13287,53 @@ export class OpenCodeController {
|
|
|
12568
13287
|
|
|
12569
13288
|
private async activateBrowserView(
|
|
12570
13289
|
viewIdArg: string,
|
|
12571
|
-
|
|
13290
|
+
peerArg: IControllerPeer,
|
|
13291
|
+
streamGenerationArg: number,
|
|
12572
13292
|
signalArg: AbortSignal,
|
|
12573
13293
|
): Promise<void> {
|
|
12574
|
-
await this.runBrowserViewSlotOperation(
|
|
13294
|
+
await this.runBrowserViewSlotOperation(peerArg.id, async (slotArg) => {
|
|
12575
13295
|
const view = slotArg.view;
|
|
13296
|
+
const transport = view?.transport;
|
|
12576
13297
|
if (
|
|
12577
13298
|
!view
|
|
12578
13299
|
|| view.id !== viewIdArg
|
|
12579
13300
|
|| view.closing
|
|
13301
|
+
|| view.transportPeer !== peerArg
|
|
13302
|
+
|| view.credentialId !== peerArg.context.state[authenticatedPeerStateKey]
|
|
13303
|
+
|| view.streamGeneration !== streamGenerationArg
|
|
13304
|
+
|| !transport
|
|
13305
|
+
|| transport.generation !== streamGenerationArg
|
|
12580
13306
|
|| this.browserViews.get(viewIdArg) !== view
|
|
12581
13307
|
) {
|
|
12582
13308
|
throw new plugins.typedrequest.TypedResponseError('The browser view is unavailable.', {
|
|
12583
13309
|
code: 'resource_unavailable',
|
|
12584
13310
|
});
|
|
12585
13311
|
}
|
|
12586
|
-
|
|
13312
|
+
await this.assertBrowserViewAuthority(
|
|
13313
|
+
view,
|
|
13314
|
+
streamGenerationArg,
|
|
13315
|
+
transport.identity,
|
|
13316
|
+
signalArg,
|
|
13317
|
+
);
|
|
13318
|
+
if (view.lifecycle === 'active' && transport.lifecycle === 'active') return;
|
|
12587
13319
|
if (view.activationPromise) {
|
|
12588
13320
|
await awaitWithAbortSignal(view.activationPromise, signalArg);
|
|
12589
13321
|
return;
|
|
12590
13322
|
}
|
|
12591
|
-
view.
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
13323
|
+
const recovering = view.lifecycle === 'recovering';
|
|
13324
|
+
if (!recovering && view.lifecycle !== 'inactive') {
|
|
13325
|
+
throw new plugins.typedrequest.TypedResponseError('The browser view cannot be activated.', {
|
|
13326
|
+
code: 'concurrent_change',
|
|
13327
|
+
});
|
|
13328
|
+
}
|
|
13329
|
+
if (!recovering) view.lifecycle = 'activating';
|
|
13330
|
+
const activation = this.activateBrowserViewTransport(view, transport, recovering, signalArg);
|
|
12596
13331
|
view.activationPromise = activation;
|
|
12597
|
-
const activationSignal = AbortSignal.any([
|
|
12598
|
-
signalArg,
|
|
12599
|
-
view.activationAbortController.signal,
|
|
12600
|
-
]);
|
|
12601
13332
|
try {
|
|
12602
|
-
await
|
|
12603
|
-
activationSignal.throwIfAborted();
|
|
13333
|
+
await activation;
|
|
12604
13334
|
if (view.closing || slotArg.view !== view) {
|
|
12605
13335
|
throw new Error('The browser view changed during activation.');
|
|
12606
13336
|
}
|
|
12607
|
-
view.activationState = 'active';
|
|
12608
13337
|
if (view.activationTimer) clearTimeout(view.activationTimer);
|
|
12609
13338
|
view.activationTimer = undefined;
|
|
12610
13339
|
} catch (errorArg) {
|
|
@@ -12616,13 +13345,175 @@ export class OpenCodeController {
|
|
|
12616
13345
|
});
|
|
12617
13346
|
}
|
|
12618
13347
|
|
|
12619
|
-
private async
|
|
12620
|
-
|
|
12621
|
-
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
13348
|
+
private async activateBrowserViewTransport(
|
|
13349
|
+
viewArg: IControllerBrowserView,
|
|
13350
|
+
transportArg: IControllerBrowserViewTransport,
|
|
13351
|
+
recoveringArg: boolean,
|
|
13352
|
+
signalArg: AbortSignal,
|
|
13353
|
+
): Promise<void> {
|
|
13354
|
+
const signals = [
|
|
13355
|
+
signalArg,
|
|
13356
|
+
viewArg.activationAbortController.signal,
|
|
13357
|
+
transportArg.abortController.signal,
|
|
13358
|
+
];
|
|
13359
|
+
if (recoveringArg && viewArg.recoveryDeadlineAt !== undefined) {
|
|
13360
|
+
const remainingMs = viewArg.recoveryDeadlineAt - Date.now();
|
|
13361
|
+
if (remainingMs <= 0) throw new Error('Browser view recovery timed out.');
|
|
13362
|
+
signals.push(AbortSignal.timeout(remainingMs));
|
|
13363
|
+
}
|
|
13364
|
+
const activationSignal = AbortSignal.any(signals);
|
|
13365
|
+
await awaitWithAbortSignal(transportArg.openingTask, activationSignal);
|
|
13366
|
+
activationSignal.throwIfAborted();
|
|
13367
|
+
if (viewArg.transport !== transportArg || transportArg.lifecycle !== 'ready') {
|
|
13368
|
+
throw new Error('The browser view transport changed during activation.');
|
|
13369
|
+
}
|
|
13370
|
+
transportArg.lifecycle = 'active';
|
|
13371
|
+
|
|
13372
|
+
if (!recoveringArg) {
|
|
13373
|
+
await awaitWithAbortSignal(viewArg.handle.activate((eventArg) => {
|
|
13374
|
+
const currentTransport = viewArg.transport;
|
|
13375
|
+
if (viewArg.closing || !currentTransport) return;
|
|
13376
|
+
this.queueBrowserViewEvent(viewArg, currentTransport, eventArg);
|
|
13377
|
+
}), activationSignal);
|
|
13378
|
+
activationSignal.throwIfAborted();
|
|
13379
|
+
viewArg.lifecycle = 'active';
|
|
13380
|
+
this.ensureBrowserOperationDrain(viewArg, transportArg);
|
|
13381
|
+
return;
|
|
13382
|
+
}
|
|
13383
|
+
|
|
13384
|
+
const runtimeState = viewArg.handle.lease.getState();
|
|
13385
|
+
const activeTab = runtimeState.tabs.find((tabArg) => tabArg.id === runtimeState.activeTabId);
|
|
13386
|
+
if (!activeTab || !activeTab.active || activeTab.status !== 'open' || !activeTab.streaming) {
|
|
13387
|
+
throw new Error('The active browser tab cannot refresh its frame stream.');
|
|
13388
|
+
}
|
|
13389
|
+
const stateDelivery = this.queueBrowserServerPacket(viewArg, transportArg, {
|
|
13390
|
+
protocol: controllerBrowserTransportProtocol,
|
|
13391
|
+
streamGeneration: transportArg.generation,
|
|
13392
|
+
type: 'state',
|
|
13393
|
+
state: this.nextControllerBrowserViewState(viewArg, runtimeState),
|
|
13394
|
+
});
|
|
13395
|
+
if (!stateDelivery) throw new Error('The recovered browser state could not be queued.');
|
|
13396
|
+
await awaitWithAbortSignal(stateDelivery, activationSignal);
|
|
13397
|
+
viewArg.recoveryFrameGate = {
|
|
13398
|
+
tabId: activeTab.id,
|
|
13399
|
+
minimumGeneration: activeTab.generation + 1,
|
|
13400
|
+
viewportRevision: runtimeState.viewportRevision,
|
|
13401
|
+
admitted: false,
|
|
13402
|
+
};
|
|
13403
|
+
await viewArg.handle.refreshFrameStream(activationSignal);
|
|
13404
|
+
if (!viewArg.recoveryFrameGate.admitted) {
|
|
13405
|
+
throw new Error('BrowserRuntime did not deliver the recovered frame boundary.');
|
|
13406
|
+
}
|
|
13407
|
+
viewArg.recoveryFrameGate = undefined;
|
|
13408
|
+
viewArg.lifecycle = 'active';
|
|
13409
|
+
viewArg.recoveryDeadlineAt = undefined;
|
|
13410
|
+
if (viewArg.recoveryTimer) clearTimeout(viewArg.recoveryTimer);
|
|
13411
|
+
viewArg.recoveryTimer = undefined;
|
|
13412
|
+
this.ensureBrowserOperationDrain(viewArg, transportArg);
|
|
13413
|
+
}
|
|
13414
|
+
|
|
13415
|
+
private async recoverBrowserView(
|
|
13416
|
+
peerArg: IControllerPeer,
|
|
13417
|
+
transportConnectionArg: plugins.typedsocket.ISmartServeConnectionWrapper,
|
|
13418
|
+
requestArg: IReq_ControllerBrowserViewRecover['request'],
|
|
13419
|
+
signalArg: AbortSignal,
|
|
13420
|
+
): Promise<plugins.typedrequest.TReverseVirtualStreamDirections<
|
|
13421
|
+
IReq_ControllerBrowserViewRecover['response']
|
|
13422
|
+
>> {
|
|
13423
|
+
return this.runBrowserViewSlotOperation(peerArg.id, async (slotArg) => {
|
|
13424
|
+
const view = slotArg.view;
|
|
13425
|
+
const oldTransport = view?.transport;
|
|
13426
|
+
if (
|
|
13427
|
+
!view
|
|
13428
|
+
|| view.id !== requestArg.viewId
|
|
13429
|
+
|| view.closing
|
|
13430
|
+
|| view.transportConnection.peer !== transportConnectionArg.peer
|
|
13431
|
+
|| view.transportPeer !== peerArg
|
|
13432
|
+
|| view.credentialId !== peerArg.context.state[authenticatedPeerStateKey]
|
|
13433
|
+
|| view.streamGeneration !== requestArg.expectedStreamGeneration
|
|
13434
|
+
|| !oldTransport
|
|
13435
|
+
|| oldTransport.generation !== requestArg.expectedStreamGeneration
|
|
13436
|
+
|| (view.lifecycle !== 'active' && view.lifecycle !== 'recoverable')
|
|
13437
|
+
) {
|
|
13438
|
+
throw new plugins.typedrequest.TypedResponseError('The browser view cannot be recovered.', {
|
|
13439
|
+
code: 'resource_unavailable',
|
|
13440
|
+
});
|
|
13441
|
+
}
|
|
13442
|
+
await this.assertBrowserViewAuthority(
|
|
13443
|
+
view,
|
|
13444
|
+
oldTransport.generation,
|
|
13445
|
+
oldTransport.identity,
|
|
13446
|
+
signalArg,
|
|
13447
|
+
);
|
|
13448
|
+
const now = Date.now();
|
|
13449
|
+
view.recoveryDeadlineAt ??= now + controllerBrowserTransportRecoveryGraceMs;
|
|
13450
|
+
if (
|
|
13451
|
+
now >= view.recoveryDeadlineAt
|
|
13452
|
+
|| view.recoveryAttempts >= controllerBrowserTransportMaximumRecoveryAttempts
|
|
13453
|
+
) {
|
|
13454
|
+
throw new plugins.typedrequest.TypedResponseError('Browser view recovery capacity was reached.', {
|
|
13455
|
+
code: 'operation_limit',
|
|
13456
|
+
});
|
|
13457
|
+
}
|
|
13458
|
+
view.recoveryAttempts += 1;
|
|
13459
|
+
view.lifecycle = 'recovering';
|
|
13460
|
+
this.armBrowserViewRecoveryDeadline(view);
|
|
13461
|
+
try {
|
|
13462
|
+
await this.retireBrowserViewTransport(
|
|
13463
|
+
view,
|
|
13464
|
+
oldTransport,
|
|
13465
|
+
Math.min(
|
|
13466
|
+
view.recoveryDeadlineAt,
|
|
13467
|
+
Date.now() + browserViewTransportCleanupTimeoutMs,
|
|
13468
|
+
),
|
|
13469
|
+
);
|
|
13470
|
+
signalArg.throwIfAborted();
|
|
13471
|
+
if (Date.now() >= view.recoveryDeadlineAt) {
|
|
13472
|
+
throw new Error('Browser view recovery timed out.');
|
|
13473
|
+
}
|
|
13474
|
+
const streamGeneration = oldTransport.generation + 1;
|
|
13475
|
+
view.streamGeneration = streamGeneration;
|
|
13476
|
+
const transport = await this.createBrowserViewTransport(view, streamGeneration);
|
|
13477
|
+
this.beginBrowserViewTransportOpening(view, transport);
|
|
13478
|
+
signalArg.throwIfAborted();
|
|
13479
|
+
return {
|
|
13480
|
+
viewId: view.id,
|
|
13481
|
+
resourceId: view.resource.id,
|
|
13482
|
+
attachmentRevision: view.resource.attachmentRevision,
|
|
13483
|
+
streamGeneration,
|
|
13484
|
+
events: transport.events,
|
|
13485
|
+
operations: transport.operations,
|
|
13486
|
+
state: this.nextControllerBrowserViewState(view, view.handle.lease.getState()),
|
|
13487
|
+
};
|
|
13488
|
+
} catch (errorArg) {
|
|
13489
|
+
this.scheduleBrowserViewClose(view, errorArg);
|
|
13490
|
+
throw asTypedResponseError(errorArg);
|
|
13491
|
+
}
|
|
13492
|
+
});
|
|
13493
|
+
}
|
|
13494
|
+
|
|
13495
|
+
private async closeBrowserView(
|
|
13496
|
+
viewIdArg: string,
|
|
13497
|
+
peerArg: string | IControllerPeer,
|
|
13498
|
+
expectedStreamGenerationArg?: number,
|
|
13499
|
+
): Promise<boolean> {
|
|
13500
|
+
const peerId = typeof peerArg === 'string' ? peerArg : peerArg.id;
|
|
13501
|
+
const slot = this.browserViewSlots.get(peerId);
|
|
13502
|
+
const view = slot?.view;
|
|
13503
|
+
if (
|
|
13504
|
+
!slot
|
|
13505
|
+
|| !view
|
|
13506
|
+
|| view.id !== viewIdArg
|
|
13507
|
+
|| (typeof peerArg !== 'string' && view.transportPeer !== peerArg)
|
|
13508
|
+
|| (
|
|
13509
|
+
expectedStreamGenerationArg !== undefined
|
|
13510
|
+
&& view.streamGeneration !== expectedStreamGenerationArg
|
|
13511
|
+
)
|
|
13512
|
+
) return false;
|
|
13513
|
+
await this.requestBrowserViewCleanup(slot, view, new Error('Browser view closed.'));
|
|
13514
|
+
return true;
|
|
13515
|
+
}
|
|
13516
|
+
|
|
12626
13517
|
private async closeBrowserViewsForPeer(peerIdArg: string): Promise<void> {
|
|
12627
13518
|
const slot = this.browserViewSlots.get(peerIdArg);
|
|
12628
13519
|
const view = slot?.view;
|
|
@@ -12694,15 +13585,22 @@ export class OpenCodeController {
|
|
|
12694
13585
|
private markBrowserViewClosing(viewArg: IControllerBrowserView, reasonArg: unknown): void {
|
|
12695
13586
|
if (!viewArg.closing) {
|
|
12696
13587
|
viewArg.closing = true;
|
|
13588
|
+
viewArg.lifecycle = 'closing';
|
|
12697
13589
|
if (this.browserViews.get(viewArg.id) === viewArg) this.browserViews.delete(viewArg.id);
|
|
12698
13590
|
if (viewArg.activationTimer) clearTimeout(viewArg.activationTimer);
|
|
12699
13591
|
viewArg.activationTimer = undefined;
|
|
13592
|
+
if (viewArg.recoveryTimer) clearTimeout(viewArg.recoveryTimer);
|
|
13593
|
+
viewArg.recoveryTimer = undefined;
|
|
12700
13594
|
if (!viewArg.activationAbortController.signal.aborted) {
|
|
12701
13595
|
viewArg.activationAbortController.abort(reasonArg);
|
|
12702
13596
|
}
|
|
12703
13597
|
}
|
|
12704
|
-
|
|
12705
|
-
|
|
13598
|
+
const transport = viewArg.transport;
|
|
13599
|
+
if (transport) {
|
|
13600
|
+
if (!transport.abortController.signal.aborted) transport.abortController.abort(reasonArg);
|
|
13601
|
+
transport.activeMutation?.abortController?.abort(reasonArg);
|
|
13602
|
+
void transport.events.abort(reasonArg).catch(() => undefined);
|
|
13603
|
+
void transport.operations.abort(reasonArg).catch(() => undefined);
|
|
12706
13604
|
}
|
|
12707
13605
|
}
|
|
12708
13606
|
|
|
@@ -12767,14 +13665,18 @@ export class OpenCodeController {
|
|
|
12767
13665
|
this.markBrowserViewClosing(viewArg, reasonArg);
|
|
12768
13666
|
if (slotArg.view !== viewArg) return;
|
|
12769
13667
|
const errors: unknown[] = [];
|
|
12770
|
-
const
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
.
|
|
13668
|
+
const constructionCleanup = viewArg.transportConstructionCleanup;
|
|
13669
|
+
if (constructionCleanup && !constructionCleanup.settled) {
|
|
13670
|
+
errors.push(new Error('Browser stream pair construction cleanup remains pending.'));
|
|
13671
|
+
}
|
|
13672
|
+
if (viewArg.transport) {
|
|
13673
|
+
await this.retireBrowserViewTransport(
|
|
13674
|
+
viewArg,
|
|
13675
|
+
viewArg.transport,
|
|
13676
|
+
Date.now() + browserViewTransportCleanupTimeoutMs,
|
|
13677
|
+
).catch((errorArg) => errors.push(errorArg));
|
|
13678
|
+
}
|
|
12776
13679
|
await viewArg.handle.close().catch((errorArg) => errors.push(errorArg));
|
|
12777
|
-
await viewArg.deliveryTail.catch((errorArg) => errors.push(errorArg));
|
|
12778
13680
|
if (errors.length === 1) throw errors[0];
|
|
12779
13681
|
if (errors.length > 1) {
|
|
12780
13682
|
throw new AggregateError(errors, 'Browser view cleanup is incomplete.');
|
|
@@ -12790,6 +13692,13 @@ export class OpenCodeController {
|
|
|
12790
13692
|
reasonArg: unknown,
|
|
12791
13693
|
): void {
|
|
12792
13694
|
if (slotArg.view !== viewArg || viewArg.cleanupRetryTimer) return;
|
|
13695
|
+
if (
|
|
13696
|
+
viewArg.transportConstructionCleanup
|
|
13697
|
+
|| (
|
|
13698
|
+
viewArg.transport?.retirementSettlement
|
|
13699
|
+
&& !viewArg.transport.retirementSettled
|
|
13700
|
+
)
|
|
13701
|
+
) return;
|
|
12793
13702
|
const timer = setTimeout(() => {
|
|
12794
13703
|
if (viewArg.cleanupRetryTimer === timer) viewArg.cleanupRetryTimer = undefined;
|
|
12795
13704
|
if (slotArg.view !== viewArg) return;
|
|
@@ -12801,250 +13710,932 @@ export class OpenCodeController {
|
|
|
12801
13710
|
timer.unref?.();
|
|
12802
13711
|
}
|
|
12803
13712
|
|
|
12804
|
-
private
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
13713
|
+
private queueBrowserViewEvent(
|
|
13714
|
+
viewArg: IControllerBrowserView,
|
|
13715
|
+
transportArg: IControllerBrowserViewTransport,
|
|
13716
|
+
eventArg: plugins.browserRuntime.TBrowserRuntimeEvent,
|
|
13717
|
+
): void {
|
|
13718
|
+
try {
|
|
13719
|
+
if (viewArg.lifecycle === 'failing' || viewArg.lifecycle === 'closing') return;
|
|
13720
|
+
if (eventArg.type === 'state') {
|
|
13721
|
+
if (viewArg.lifecycle === 'recoverable') return;
|
|
13722
|
+
this.queueBrowserServerPacket(viewArg, transportArg, {
|
|
13723
|
+
protocol: controllerBrowserTransportProtocol,
|
|
13724
|
+
streamGeneration: transportArg.generation,
|
|
13725
|
+
type: 'state',
|
|
13726
|
+
state: this.nextControllerBrowserViewState(viewArg, eventArg.state),
|
|
13727
|
+
});
|
|
13728
|
+
return;
|
|
13729
|
+
}
|
|
13730
|
+
if (eventArg.type === 'error') {
|
|
13731
|
+
const delivery = this.queueBrowserServerPacket(viewArg, transportArg, {
|
|
13732
|
+
protocol: controllerBrowserTransportProtocol,
|
|
13733
|
+
streamGeneration: transportArg.generation,
|
|
13734
|
+
type: 'error',
|
|
13735
|
+
error: { ...eventArg.error, message: eventArg.error.code },
|
|
13736
|
+
});
|
|
13737
|
+
if (eventArg.error.fatal) {
|
|
13738
|
+
const reason = new Error(eventArg.error.code);
|
|
13739
|
+
viewArg.lifecycle = 'failing';
|
|
13740
|
+
this.terminalizeBrowserTransportOperations(transportArg, reason);
|
|
13741
|
+
if (!delivery) {
|
|
13742
|
+
this.scheduleBrowserViewClose(viewArg, reason);
|
|
13743
|
+
} else {
|
|
13744
|
+
void awaitWithAbortSignal(
|
|
13745
|
+
delivery,
|
|
13746
|
+
AbortSignal.timeout(browserViewTransportCleanupTimeoutMs),
|
|
13747
|
+
).then(
|
|
13748
|
+
() => this.scheduleBrowserViewClose(viewArg, reason),
|
|
13749
|
+
() => this.scheduleBrowserViewClose(viewArg, reason),
|
|
13750
|
+
);
|
|
13751
|
+
}
|
|
13752
|
+
}
|
|
13753
|
+
return;
|
|
13754
|
+
}
|
|
13755
|
+
if (viewArg.lifecycle === 'recoverable') return;
|
|
13756
|
+
const gate = viewArg.recoveryFrameGate;
|
|
13757
|
+
if (viewArg.lifecycle === 'recovering') {
|
|
13758
|
+
if (
|
|
13759
|
+
!gate
|
|
13760
|
+
|| eventArg.frame.tabId !== gate.tabId
|
|
13761
|
+
|| eventArg.frame.generation < gate.minimumGeneration
|
|
13762
|
+
|| eventArg.frame.viewportRevision !== gate.viewportRevision
|
|
13763
|
+
) return;
|
|
13764
|
+
}
|
|
13765
|
+
const { data, ...frame } = eventArg.frame;
|
|
13766
|
+
if (data.byteLength > controllerBrowserTransportMaximumFrameBytes) {
|
|
13767
|
+
throw new Error('The browser frame exceeds the transport byte limit.');
|
|
13768
|
+
}
|
|
13769
|
+
const copiedData = Uint8Array.from(data);
|
|
13770
|
+
const metadata: IControllerBrowserFrameMetadata = {
|
|
13771
|
+
...frame,
|
|
13772
|
+
dataByteLength: copiedData.byteLength,
|
|
13773
|
+
};
|
|
13774
|
+
this.queueBrowserServerPacket(viewArg, transportArg, {
|
|
13775
|
+
protocol: controllerBrowserTransportProtocol,
|
|
13776
|
+
streamGeneration: transportArg.generation,
|
|
13777
|
+
type: 'frame',
|
|
13778
|
+
frame: metadata,
|
|
13779
|
+
}, copiedData);
|
|
13780
|
+
if (gate) gate.admitted = true;
|
|
13781
|
+
} catch (errorArg) {
|
|
13782
|
+
this.scheduleBrowserViewClose(viewArg, errorArg);
|
|
13783
|
+
throw errorArg;
|
|
13784
|
+
}
|
|
13785
|
+
}
|
|
13786
|
+
|
|
13787
|
+
private async createBrowserViewTransport(
|
|
13788
|
+
viewArg: IControllerBrowserView,
|
|
13789
|
+
streamGenerationArg: number,
|
|
13790
|
+
): Promise<IControllerBrowserViewTransport> {
|
|
13791
|
+
const typedSocket = this.typedServer?.typedsocket;
|
|
13792
|
+
if (!typedSocket) throw new Error('The controller transport is unavailable.');
|
|
13793
|
+
const transportIdentity = Symbol('controller.browser.transport');
|
|
13794
|
+
if (viewArg.pendingTransportIdentity || viewArg.transportConstructionCleanup) {
|
|
13795
|
+
throw new Error('A browser stream pair is already being constructed.');
|
|
13796
|
+
}
|
|
13797
|
+
viewArg.pendingTransportIdentity = transportIdentity;
|
|
13798
|
+
const createAuthorization = (
|
|
13799
|
+
creatorDirectionArg: plugins.typedrequestInterfaces.TVirtualStreamDirection,
|
|
13800
|
+
contentTypeArg: string,
|
|
13801
|
+
): IControllerBrowserStreamAuthorization => Object.freeze({
|
|
13802
|
+
marker: browserStreamAuthorizationMarker,
|
|
13803
|
+
view: viewArg,
|
|
13804
|
+
authority: viewArg.authority,
|
|
13805
|
+
transportPeer: viewArg.transportPeer,
|
|
13806
|
+
transportIdentity,
|
|
13807
|
+
streamGeneration: streamGenerationArg,
|
|
13808
|
+
creatorDirection: creatorDirectionArg,
|
|
13809
|
+
contentType: contentTypeArg,
|
|
13810
|
+
});
|
|
13811
|
+
try {
|
|
13812
|
+
const events = typedSocket.createVirtualStream({
|
|
13813
|
+
target: viewArg.transportConnection,
|
|
13814
|
+
creatorDirection: 'send',
|
|
13815
|
+
contentType: controllerBrowserTransportEventContentType,
|
|
13816
|
+
authorization: createAuthorization('send', controllerBrowserTransportEventContentType),
|
|
12814
13817
|
});
|
|
13818
|
+
let operations: plugins.typedrequestInterfaces.TVirtualStream<'receive'>;
|
|
13819
|
+
try {
|
|
13820
|
+
operations = typedSocket.createVirtualStream({
|
|
13821
|
+
target: viewArg.transportConnection,
|
|
13822
|
+
creatorDirection: 'receive',
|
|
13823
|
+
contentType: controllerBrowserTransportOperationContentType,
|
|
13824
|
+
authorization: createAuthorization('receive', controllerBrowserTransportOperationContentType),
|
|
13825
|
+
});
|
|
13826
|
+
} catch (errorArg) {
|
|
13827
|
+
let cleanup!: IControllerBrowserTransportConstructionCleanup;
|
|
13828
|
+
const settlement = Promise.allSettled([
|
|
13829
|
+
Promise.resolve().then(() => events.abort(errorArg)),
|
|
13830
|
+
events.opened,
|
|
13831
|
+
events.completion,
|
|
13832
|
+
events.closed,
|
|
13833
|
+
]).then(() => {
|
|
13834
|
+
cleanup.settled = true;
|
|
13835
|
+
const slot = this.browserViewSlots.get(viewArg.peerId);
|
|
13836
|
+
if (
|
|
13837
|
+
!cleanup.deadlineExpired
|
|
13838
|
+
|| !viewArg.closing
|
|
13839
|
+
|| slot?.view !== viewArg
|
|
13840
|
+
) {
|
|
13841
|
+
if (viewArg.transportConstructionCleanup === cleanup) {
|
|
13842
|
+
viewArg.transportConstructionCleanup = undefined;
|
|
13843
|
+
}
|
|
13844
|
+
return;
|
|
13845
|
+
}
|
|
13846
|
+
const activeCleanupTask = viewArg.cleanupTask;
|
|
13847
|
+
void (activeCleanupTask
|
|
13848
|
+
? activeCleanupTask.catch(() => undefined)
|
|
13849
|
+
: Promise.resolve()
|
|
13850
|
+
).then(() => {
|
|
13851
|
+
const currentSlot = this.browserViewSlots.get(viewArg.peerId);
|
|
13852
|
+
if (
|
|
13853
|
+
viewArg.transportConstructionCleanup !== cleanup
|
|
13854
|
+
|| !viewArg.closing
|
|
13855
|
+
|| currentSlot?.view !== viewArg
|
|
13856
|
+
) return;
|
|
13857
|
+
viewArg.transportConstructionCleanup = undefined;
|
|
13858
|
+
void this.requestBrowserViewCleanup(currentSlot, viewArg, cleanup.reason).catch(
|
|
13859
|
+
(cleanupErrorArg) => {
|
|
13860
|
+
console.error(`Browser view ${viewArg.id} late construction cleanup failed.`, cleanupErrorArg);
|
|
13861
|
+
},
|
|
13862
|
+
);
|
|
13863
|
+
});
|
|
13864
|
+
});
|
|
13865
|
+
cleanup = {
|
|
13866
|
+
settlement,
|
|
13867
|
+
reason: errorArg,
|
|
13868
|
+
settled: false,
|
|
13869
|
+
deadlineExpired: false,
|
|
13870
|
+
};
|
|
13871
|
+
viewArg.transportConstructionCleanup = cleanup;
|
|
13872
|
+
try {
|
|
13873
|
+
await this.waitForBrowserTransportSettlement(
|
|
13874
|
+
settlement,
|
|
13875
|
+
Date.now() + browserViewTransportCleanupTimeoutMs,
|
|
13876
|
+
);
|
|
13877
|
+
} catch (cleanupErrorArg) {
|
|
13878
|
+
cleanup.deadlineExpired = true;
|
|
13879
|
+
throw new AggregateError(
|
|
13880
|
+
[errorArg, cleanupErrorArg],
|
|
13881
|
+
'Browser stream pair construction cleanup is incomplete.',
|
|
13882
|
+
);
|
|
13883
|
+
}
|
|
13884
|
+
throw errorArg;
|
|
13885
|
+
}
|
|
13886
|
+
const transport: IControllerBrowserViewTransport = {
|
|
13887
|
+
identity: transportIdentity,
|
|
13888
|
+
generation: streamGenerationArg,
|
|
13889
|
+
lifecycle: 'opening',
|
|
13890
|
+
events,
|
|
13891
|
+
operations,
|
|
13892
|
+
abortController: new AbortController(),
|
|
13893
|
+
openingTask: Promise.resolve(),
|
|
13894
|
+
deliveryTail: Promise.resolve(),
|
|
13895
|
+
pendingEventCount: 0,
|
|
13896
|
+
pendingEventBytes: 0,
|
|
13897
|
+
operationQueue: [],
|
|
13898
|
+
queuedOperationBytes: 0,
|
|
13899
|
+
highestOperationId: 0,
|
|
13900
|
+
cancellableOperationIds: new Set(),
|
|
13901
|
+
activeAcknowledgements: new Map(),
|
|
13902
|
+
};
|
|
13903
|
+
viewArg.transport = transport;
|
|
13904
|
+
return transport;
|
|
13905
|
+
} finally {
|
|
13906
|
+
if (viewArg.pendingTransportIdentity === transportIdentity) {
|
|
13907
|
+
viewArg.pendingTransportIdentity = undefined;
|
|
13908
|
+
}
|
|
12815
13909
|
}
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
13910
|
+
}
|
|
13911
|
+
|
|
13912
|
+
private beginBrowserViewTransportOpening(
|
|
13913
|
+
viewArg: IControllerBrowserView,
|
|
13914
|
+
transportArg: IControllerBrowserViewTransport,
|
|
13915
|
+
): void {
|
|
13916
|
+
if (viewArg.transport !== transportArg) {
|
|
13917
|
+
throw new Error('The browser stream pair cannot open before publication.');
|
|
13918
|
+
}
|
|
13919
|
+
const openingTask = this.openBrowserViewTransport(viewArg, transportArg);
|
|
13920
|
+
transportArg.openingTask = openingTask;
|
|
13921
|
+
void openingTask.catch((errorArg) => {
|
|
13922
|
+
this.noteBrowserTransportFault(viewArg, transportArg, errorArg, true);
|
|
13923
|
+
});
|
|
13924
|
+
}
|
|
13925
|
+
|
|
13926
|
+
private async openBrowserViewTransport(
|
|
13927
|
+
viewArg: IControllerBrowserView,
|
|
13928
|
+
transportArg: IControllerBrowserViewTransport,
|
|
13929
|
+
): Promise<void> {
|
|
13930
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
13931
|
+
try {
|
|
13932
|
+
await Promise.race([
|
|
13933
|
+
Promise.all([transportArg.events.opened, transportArg.operations.opened]),
|
|
13934
|
+
new Promise<never>((_resolve, reject) => {
|
|
13935
|
+
timeout = setTimeout(() => reject(new Error('Browser stream pair opening timed out.')),
|
|
13936
|
+
controllerBrowserTransportPairOpenTimeoutMs);
|
|
13937
|
+
timeout.unref?.();
|
|
13938
|
+
}),
|
|
13939
|
+
]);
|
|
13940
|
+
if (
|
|
13941
|
+
viewArg.transport !== transportArg
|
|
13942
|
+
|| viewArg.closing
|
|
13943
|
+
|| viewArg.streamGeneration !== transportArg.generation
|
|
13944
|
+
|| transportArg.lifecycle !== 'opening'
|
|
13945
|
+
) throw new Error('The browser stream pair became stale while opening.');
|
|
13946
|
+
transportArg.lifecycle = 'ready';
|
|
13947
|
+
const receiveTask = this.receiveBrowserOperations(viewArg, transportArg);
|
|
13948
|
+
transportArg.receiveTask = receiveTask;
|
|
13949
|
+
void receiveTask.catch((errorArg) => {
|
|
13950
|
+
this.noteBrowserTransportFault(viewArg, transportArg, errorArg, true);
|
|
13951
|
+
});
|
|
13952
|
+
} finally {
|
|
13953
|
+
if (timeout) clearTimeout(timeout);
|
|
13954
|
+
}
|
|
13955
|
+
}
|
|
13956
|
+
|
|
13957
|
+
private async receiveBrowserOperations(
|
|
13958
|
+
viewArg: IControllerBrowserView,
|
|
13959
|
+
transportArg: IControllerBrowserViewTransport,
|
|
13960
|
+
): Promise<void> {
|
|
13961
|
+
while (transportArg.lifecycle !== 'retiring' && transportArg.lifecycle !== 'retired') {
|
|
13962
|
+
const chunk = await transportArg.operations.receive();
|
|
13963
|
+
if (chunk === undefined) {
|
|
13964
|
+
if (transportArg.abortController.signal.aborted) return;
|
|
13965
|
+
await transportArg.operations.accept();
|
|
13966
|
+
throw new Error('The browser operation stream closed unexpectedly.');
|
|
13967
|
+
}
|
|
13968
|
+
let packet: TControllerBrowserClientPacket;
|
|
13969
|
+
try {
|
|
13970
|
+
packet = assertControllerBrowserClientPacket(
|
|
13971
|
+
decodeControllerBrowserTransportPacket(chunk),
|
|
13972
|
+
);
|
|
13973
|
+
} catch (errorArg) {
|
|
13974
|
+
this.failBrowserTransportProtocol(viewArg, transportArg, errorArg);
|
|
13975
|
+
return;
|
|
13976
|
+
}
|
|
13977
|
+
if (packet.streamGeneration !== transportArg.generation) {
|
|
13978
|
+
this.failBrowserTransportProtocol(
|
|
13979
|
+
viewArg,
|
|
13980
|
+
transportArg,
|
|
13981
|
+
new Error('The browser operation belongs to a stale stream generation.'),
|
|
13982
|
+
);
|
|
13983
|
+
return;
|
|
13984
|
+
}
|
|
13985
|
+
this.admitBrowserClientPacket(viewArg, transportArg, packet, chunk.byteLength);
|
|
13986
|
+
}
|
|
13987
|
+
}
|
|
13988
|
+
|
|
13989
|
+
private admitBrowserClientPacket(
|
|
13990
|
+
viewArg: IControllerBrowserView,
|
|
13991
|
+
transportArg: IControllerBrowserViewTransport,
|
|
13992
|
+
packetArg: TControllerBrowserClientPacket,
|
|
13993
|
+
byteLengthArg: number,
|
|
13994
|
+
): void {
|
|
13995
|
+
if (packetArg.type === 'cancel') {
|
|
13996
|
+
if (packetArg.operationId > transportArg.highestOperationId) {
|
|
13997
|
+
this.failBrowserTransportProtocol(viewArg, transportArg, new Error('cancel targeted a future operation.'));
|
|
13998
|
+
return;
|
|
13999
|
+
}
|
|
14000
|
+
if (transportArg.cancellableOperationIds.has(packetArg.operationId)) {
|
|
14001
|
+
this.cancelBrowserOperation(viewArg, transportArg, packetArg.operationId);
|
|
14002
|
+
}
|
|
14003
|
+
return;
|
|
14004
|
+
}
|
|
14005
|
+
const admitted = this.consumeBrowserRealtimeAdmission(viewArg.peerId);
|
|
14006
|
+
if (!admitted) {
|
|
14007
|
+
if (packetArg.operationId <= transportArg.highestOperationId) {
|
|
14008
|
+
this.failBrowserTransportProtocol(viewArg, transportArg, new Error('operationId was reused.'));
|
|
14009
|
+
return;
|
|
14010
|
+
}
|
|
14011
|
+
transportArg.highestOperationId = packetArg.operationId;
|
|
14012
|
+
this.queueBrowserOperationResult(viewArg, transportArg, packetArg.operationId, {
|
|
14013
|
+
status: 'failed',
|
|
14014
|
+
error: { code: 'rate_limited', message: 'The browser input rate limit was reached.' },
|
|
14015
|
+
});
|
|
14016
|
+
return;
|
|
14017
|
+
}
|
|
14018
|
+
if (packetArg.operationId <= transportArg.highestOperationId) {
|
|
14019
|
+
this.failBrowserTransportProtocol(viewArg, transportArg, new Error('operationId was reused.'));
|
|
14020
|
+
return;
|
|
14021
|
+
}
|
|
14022
|
+
transportArg.highestOperationId = packetArg.operationId;
|
|
14023
|
+
const queued: IControllerBrowserQueuedOperation = {
|
|
14024
|
+
operationId: packetArg.operationId,
|
|
14025
|
+
operation: packetArg.operation,
|
|
14026
|
+
byteLength: byteLengthArg,
|
|
14027
|
+
cancelled: false,
|
|
14028
|
+
terminal: false,
|
|
14029
|
+
};
|
|
12820
14030
|
if (
|
|
12821
|
-
|
|
12822
|
-
||
|
|
12823
|
-
|
|
12824
|
-
|| current.pendingAttachment
|
|
12825
|
-
|| current.attachmentAuthorityId !== view.resource.attachmentAuthorityId
|
|
12826
|
-
|| current.attachmentRevision !== view.resource.attachmentRevision
|
|
14031
|
+
transportArg.operationQueue.length >= controllerBrowserTransportMaximumPendingOperations
|
|
14032
|
+
|| transportArg.queuedOperationBytes + byteLengthArg
|
|
14033
|
+
> controllerBrowserTransportMaximumQueuedOperationBytes
|
|
12827
14034
|
) {
|
|
12828
|
-
|
|
12829
|
-
|
|
12830
|
-
code: '
|
|
14035
|
+
this.settleBrowserOperation(viewArg, transportArg, queued, {
|
|
14036
|
+
status: 'failed',
|
|
14037
|
+
error: { code: 'operation_limit', message: 'The browser operation queue is full.' },
|
|
12831
14038
|
});
|
|
14039
|
+
return;
|
|
12832
14040
|
}
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
await lease.dispatchKey(operationArg.input, { signal: signalArg });
|
|
12853
|
-
break;
|
|
12854
|
-
case 'insertText':
|
|
12855
|
-
await lease.insertText(operationArg.input, { signal: signalArg });
|
|
12856
|
-
break;
|
|
12857
|
-
case 'navigate':
|
|
12858
|
-
await lease.executeHumanAction({
|
|
12859
|
-
action: 'navigate',
|
|
12860
|
-
url: operationArg.url,
|
|
12861
|
-
...(operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId }),
|
|
12862
|
-
...(operationArg.timeoutMs === undefined ? {} : { timeoutMs: operationArg.timeoutMs }),
|
|
12863
|
-
}, { signal: signalArg });
|
|
12864
|
-
break;
|
|
12865
|
-
case 'createTab':
|
|
12866
|
-
await lease.createTab({
|
|
12867
|
-
...(operationArg.url === undefined ? {} : { url: operationArg.url }),
|
|
12868
|
-
...(operationArg.activate === undefined ? {} : { activate: operationArg.activate }),
|
|
12869
|
-
}, { signal: signalArg });
|
|
12870
|
-
break;
|
|
12871
|
-
case 'activateTab':
|
|
12872
|
-
await lease.activateTab(operationArg.tabId, { signal: signalArg });
|
|
12873
|
-
break;
|
|
12874
|
-
case 'closeTab':
|
|
12875
|
-
await lease.closeTab(operationArg.tabId, { signal: signalArg });
|
|
12876
|
-
break;
|
|
12877
|
-
case 'back':
|
|
12878
|
-
await lease.back(
|
|
12879
|
-
operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
|
|
12880
|
-
{ signal: signalArg },
|
|
12881
|
-
);
|
|
12882
|
-
break;
|
|
12883
|
-
case 'forward':
|
|
12884
|
-
await lease.forward(
|
|
12885
|
-
operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
|
|
12886
|
-
{ signal: signalArg },
|
|
12887
|
-
);
|
|
12888
|
-
break;
|
|
12889
|
-
case 'reload':
|
|
12890
|
-
await lease.reload(
|
|
12891
|
-
operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
|
|
12892
|
-
{ signal: signalArg },
|
|
12893
|
-
);
|
|
12894
|
-
break;
|
|
14041
|
+
transportArg.cancellableOperationIds.add(queued.operationId);
|
|
14042
|
+
const tail = transportArg.operationQueue.at(-1);
|
|
14043
|
+
if (tail && this.browserOperationsCanCoalesce(tail.operation, queued.operation)) {
|
|
14044
|
+
transportArg.operationQueue.pop();
|
|
14045
|
+
transportArg.queuedOperationBytes -= tail.byteLength;
|
|
14046
|
+
this.settleBrowserOperation(viewArg, transportArg, tail, { status: 'superseded' });
|
|
14047
|
+
}
|
|
14048
|
+
transportArg.operationQueue.push(queued);
|
|
14049
|
+
transportArg.queuedOperationBytes += byteLengthArg;
|
|
14050
|
+
if (viewArg.lifecycle === 'active' && transportArg.lifecycle === 'active') {
|
|
14051
|
+
if (queued.operation.type === 'acknowledgeFrame') {
|
|
14052
|
+
const index = transportArg.operationQueue.indexOf(queued);
|
|
14053
|
+
if (index >= 0) {
|
|
14054
|
+
transportArg.operationQueue.splice(index, 1);
|
|
14055
|
+
transportArg.queuedOperationBytes -= queued.byteLength;
|
|
14056
|
+
this.startBrowserAcknowledgement(viewArg, transportArg, queued);
|
|
14057
|
+
}
|
|
14058
|
+
} else {
|
|
14059
|
+
this.ensureBrowserOperationDrain(viewArg, transportArg);
|
|
12895
14060
|
}
|
|
12896
|
-
} catch (errorArg) {
|
|
12897
|
-
throw asTypedResponseError(errorArg);
|
|
12898
14061
|
}
|
|
12899
|
-
return { accepted, state: this.toControllerBrowserViewState(lease.getState()) };
|
|
12900
14062
|
}
|
|
12901
14063
|
|
|
12902
|
-
private
|
|
14064
|
+
private consumeBrowserRealtimeAdmission(peerIdArg: string): boolean {
|
|
14065
|
+
let peerBucket = this.browserViewRealtimeBucketsByPeer.get(peerIdArg);
|
|
14066
|
+
if (!peerBucket) {
|
|
14067
|
+
peerBucket = new ControllerBrowserTokenBucket(
|
|
14068
|
+
browserViewRealtimePeerRate,
|
|
14069
|
+
browserViewRealtimePeerBurst,
|
|
14070
|
+
);
|
|
14071
|
+
this.browserViewRealtimeBucketsByPeer.set(peerIdArg, peerBucket);
|
|
14072
|
+
}
|
|
14073
|
+
const now = performance.now();
|
|
14074
|
+
return peerBucket.consume(now) && this.browserViewRealtimeGlobalBucket.consume(now);
|
|
14075
|
+
}
|
|
14076
|
+
|
|
14077
|
+
private browserOperationsCanCoalesce(
|
|
14078
|
+
existingArg: TControllerBrowserViewOperation,
|
|
14079
|
+
candidateArg: TControllerBrowserViewOperation,
|
|
14080
|
+
): boolean {
|
|
14081
|
+
if (existingArg.type === 'setViewport' && candidateArg.type === 'setViewport') return true;
|
|
14082
|
+
return existingArg.type === 'dispatchMouse'
|
|
14083
|
+
&& candidateArg.type === 'dispatchMouse'
|
|
14084
|
+
&& existingArg.input.type === 'move'
|
|
14085
|
+
&& candidateArg.input.type === 'move'
|
|
14086
|
+
&& existingArg.input.tabId === candidateArg.input.tabId
|
|
14087
|
+
&& existingArg.input.generation === candidateArg.input.generation
|
|
14088
|
+
&& existingArg.input.viewportRevision === candidateArg.input.viewportRevision;
|
|
14089
|
+
}
|
|
14090
|
+
|
|
14091
|
+
private cancelBrowserOperation(
|
|
12903
14092
|
viewArg: IControllerBrowserView,
|
|
12904
|
-
|
|
14093
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14094
|
+
operationIdArg: number,
|
|
14095
|
+
): void {
|
|
14096
|
+
if (!transportArg.cancellableOperationIds.delete(operationIdArg)) return;
|
|
14097
|
+
const queuedIndex = transportArg.operationQueue.findIndex(
|
|
14098
|
+
(candidateArg) => candidateArg.operationId === operationIdArg,
|
|
14099
|
+
);
|
|
14100
|
+
if (queuedIndex >= 0) {
|
|
14101
|
+
const [queued] = transportArg.operationQueue.splice(queuedIndex, 1);
|
|
14102
|
+
if (!queued) return;
|
|
14103
|
+
transportArg.queuedOperationBytes -= queued.byteLength;
|
|
14104
|
+
queued.cancelled = true;
|
|
14105
|
+
this.settleBrowserOperation(viewArg, transportArg, queued, { status: 'cancelled' });
|
|
14106
|
+
return;
|
|
14107
|
+
}
|
|
14108
|
+
const active = transportArg.activeMutation;
|
|
14109
|
+
if (active?.operationId === operationIdArg && !active.terminal) {
|
|
14110
|
+
active.cancelled = true;
|
|
14111
|
+
active.abortController?.abort(new Error('The browser operation was cancelled.'));
|
|
14112
|
+
return;
|
|
14113
|
+
}
|
|
14114
|
+
const acknowledgement = transportArg.activeAcknowledgements.get(operationIdArg);
|
|
14115
|
+
if (acknowledgement && !acknowledgement.terminal) {
|
|
14116
|
+
acknowledgement.terminal = true;
|
|
14117
|
+
this.queueBrowserOperationResult(viewArg, transportArg, operationIdArg, {
|
|
14118
|
+
status: 'cancelled',
|
|
14119
|
+
});
|
|
14120
|
+
}
|
|
14121
|
+
}
|
|
14122
|
+
|
|
14123
|
+
private ensureBrowserOperationDrain(
|
|
14124
|
+
viewArg: IControllerBrowserView,
|
|
14125
|
+
transportArg: IControllerBrowserViewTransport,
|
|
12905
14126
|
): void {
|
|
12906
|
-
const eventBytes = eventArg.type === 'frame' ? eventArg.frame.data.byteLength : 0;
|
|
12907
14127
|
if (
|
|
12908
|
-
|
|
12909
|
-
||
|
|
12910
|
-
||
|
|
14128
|
+
transportArg.drainTask
|
|
14129
|
+
|| viewArg.lifecycle !== 'active'
|
|
14130
|
+
|| transportArg.lifecycle !== 'active'
|
|
14131
|
+
) return;
|
|
14132
|
+
const drainTask = this.drainBrowserOperations(viewArg, transportArg).finally(() => {
|
|
14133
|
+
if (transportArg.drainTask === drainTask) transportArg.drainTask = undefined;
|
|
14134
|
+
if (
|
|
14135
|
+
viewArg.lifecycle === 'active'
|
|
14136
|
+
&& transportArg.lifecycle === 'active'
|
|
14137
|
+
&& transportArg.operationQueue.length > 0
|
|
14138
|
+
) this.ensureBrowserOperationDrain(viewArg, transportArg);
|
|
14139
|
+
});
|
|
14140
|
+
transportArg.drainTask = drainTask;
|
|
14141
|
+
void drainTask.catch((errorArg) => {
|
|
14142
|
+
this.noteBrowserTransportFault(viewArg, transportArg, errorArg, false);
|
|
14143
|
+
});
|
|
14144
|
+
}
|
|
14145
|
+
|
|
14146
|
+
private async drainBrowserOperations(
|
|
14147
|
+
viewArg: IControllerBrowserView,
|
|
14148
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14149
|
+
): Promise<void> {
|
|
14150
|
+
while (
|
|
14151
|
+
viewArg.lifecycle === 'active'
|
|
14152
|
+
&& transportArg.lifecycle === 'active'
|
|
14153
|
+
&& viewArg.transport === transportArg
|
|
12911
14154
|
) {
|
|
12912
|
-
|
|
14155
|
+
const queued = transportArg.operationQueue.shift();
|
|
14156
|
+
if (!queued) return;
|
|
14157
|
+
transportArg.queuedOperationBytes -= queued.byteLength;
|
|
14158
|
+
if (queued.terminal) continue;
|
|
14159
|
+
if (queued.operation.type === 'acknowledgeFrame') {
|
|
14160
|
+
this.startBrowserAcknowledgement(viewArg, transportArg, queued);
|
|
14161
|
+
continue;
|
|
14162
|
+
}
|
|
14163
|
+
transportArg.activeMutation = queued;
|
|
14164
|
+
queued.abortController = new AbortController();
|
|
14165
|
+
const peerAbortController = viewArg.transportPeer.context.state[
|
|
14166
|
+
operationAbortControllerPeerStateKey
|
|
14167
|
+
];
|
|
14168
|
+
const operationSignal = AbortSignal.any([
|
|
14169
|
+
queued.abortController.signal,
|
|
14170
|
+
viewArg.activationAbortController.signal,
|
|
14171
|
+
transportArg.abortController.signal,
|
|
14172
|
+
this.operationAbortController.signal,
|
|
14173
|
+
...(peerAbortController instanceof AbortController ? [peerAbortController.signal] : []),
|
|
14174
|
+
]);
|
|
14175
|
+
try {
|
|
14176
|
+
const state = await this.runBrowserMutationOperation(
|
|
14177
|
+
viewArg,
|
|
14178
|
+
transportArg,
|
|
14179
|
+
queued.operation,
|
|
14180
|
+
operationSignal,
|
|
14181
|
+
);
|
|
14182
|
+
this.settleBrowserOperation(viewArg, transportArg, queued, {
|
|
14183
|
+
status: 'succeeded',
|
|
14184
|
+
...(state === undefined ? {} : { state }),
|
|
14185
|
+
});
|
|
14186
|
+
} catch (errorArg) {
|
|
14187
|
+
this.settleBrowserOperation(
|
|
14188
|
+
viewArg,
|
|
14189
|
+
transportArg,
|
|
14190
|
+
queued,
|
|
14191
|
+
queued.cancelled || operationSignal.aborted
|
|
14192
|
+
? { status: 'cancelled' }
|
|
14193
|
+
: { status: 'failed', error: this.browserOperationError(errorArg) },
|
|
14194
|
+
);
|
|
14195
|
+
} finally {
|
|
14196
|
+
if (transportArg.activeMutation === queued) transportArg.activeMutation = undefined;
|
|
14197
|
+
}
|
|
14198
|
+
}
|
|
14199
|
+
}
|
|
14200
|
+
|
|
14201
|
+
private async runBrowserMutationOperation(
|
|
14202
|
+
viewArg: IControllerBrowserView,
|
|
14203
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14204
|
+
operationArg: Exclude<TControllerBrowserViewOperation, { type: 'acknowledgeFrame' }>,
|
|
14205
|
+
signalArg: AbortSignal,
|
|
14206
|
+
): Promise<IControllerBrowserViewState | undefined> {
|
|
14207
|
+
const execute = async (operationSignalArg: AbortSignal) => {
|
|
14208
|
+
await this.assertBrowserViewAuthority(
|
|
14209
|
+
viewArg,
|
|
14210
|
+
transportArg.generation,
|
|
14211
|
+
transportArg.identity,
|
|
14212
|
+
operationSignalArg,
|
|
14213
|
+
);
|
|
14214
|
+
await this.executeBrowserMutation(viewArg, operationArg, operationSignalArg);
|
|
14215
|
+
};
|
|
14216
|
+
if (this.isBrowserSemanticOperation(operationArg)) {
|
|
14217
|
+
await this.runOperation(viewArg.transportPeer, execute, signalArg);
|
|
14218
|
+
return this.nextControllerBrowserViewState(viewArg, viewArg.handle.lease.getState());
|
|
14219
|
+
}
|
|
14220
|
+
if (this.lifecycleState !== 'ready' || this.stopRequested) {
|
|
14221
|
+
throw new Error('The controller is not ready to accept browser input.');
|
|
14222
|
+
}
|
|
14223
|
+
await execute(signalArg);
|
|
14224
|
+
return undefined;
|
|
14225
|
+
}
|
|
14226
|
+
|
|
14227
|
+
private isBrowserSemanticOperation(operationArg: TControllerBrowserViewOperation): boolean {
|
|
14228
|
+
return operationArg.type === 'navigate'
|
|
14229
|
+
|| operationArg.type === 'createTab'
|
|
14230
|
+
|| operationArg.type === 'activateTab'
|
|
14231
|
+
|| operationArg.type === 'closeTab'
|
|
14232
|
+
|| operationArg.type === 'back'
|
|
14233
|
+
|| operationArg.type === 'forward'
|
|
14234
|
+
|| operationArg.type === 'reload';
|
|
14235
|
+
}
|
|
14236
|
+
|
|
14237
|
+
private async executeBrowserMutation(
|
|
14238
|
+
viewArg: IControllerBrowserView,
|
|
14239
|
+
operationArg: Exclude<TControllerBrowserViewOperation, { type: 'acknowledgeFrame' }>,
|
|
14240
|
+
signalArg: AbortSignal,
|
|
14241
|
+
): Promise<void> {
|
|
14242
|
+
const lease = viewArg.handle.lease;
|
|
14243
|
+
switch (operationArg.type) {
|
|
14244
|
+
case 'setViewport':
|
|
14245
|
+
await lease.setViewport(operationArg.viewport, { signal: signalArg });
|
|
14246
|
+
return;
|
|
14247
|
+
case 'dispatchMouse':
|
|
14248
|
+
await lease.dispatchMouse(operationArg.input, { signal: signalArg });
|
|
14249
|
+
return;
|
|
14250
|
+
case 'dispatchWheel':
|
|
14251
|
+
await lease.dispatchWheel(operationArg.input, { signal: signalArg });
|
|
14252
|
+
return;
|
|
14253
|
+
case 'dispatchKey':
|
|
14254
|
+
await lease.dispatchKey(operationArg.input, { signal: signalArg });
|
|
14255
|
+
return;
|
|
14256
|
+
case 'insertText':
|
|
14257
|
+
await lease.insertText(operationArg.input, { signal: signalArg });
|
|
14258
|
+
return;
|
|
14259
|
+
case 'navigate':
|
|
14260
|
+
await lease.executeHumanAction({
|
|
14261
|
+
action: 'navigate',
|
|
14262
|
+
url: operationArg.url,
|
|
14263
|
+
...(operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId }),
|
|
14264
|
+
...(operationArg.timeoutMs === undefined ? {} : { timeoutMs: operationArg.timeoutMs }),
|
|
14265
|
+
}, { signal: signalArg });
|
|
14266
|
+
return;
|
|
14267
|
+
case 'createTab':
|
|
14268
|
+
await lease.createTab({
|
|
14269
|
+
...(operationArg.url === undefined ? {} : { url: operationArg.url }),
|
|
14270
|
+
...(operationArg.activate === undefined ? {} : { activate: operationArg.activate }),
|
|
14271
|
+
}, { signal: signalArg });
|
|
14272
|
+
return;
|
|
14273
|
+
case 'activateTab':
|
|
14274
|
+
await lease.activateTab(operationArg.tabId, { signal: signalArg });
|
|
14275
|
+
return;
|
|
14276
|
+
case 'closeTab':
|
|
14277
|
+
await lease.closeTab(operationArg.tabId, { signal: signalArg });
|
|
14278
|
+
return;
|
|
14279
|
+
case 'back':
|
|
14280
|
+
await lease.back(
|
|
14281
|
+
operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
|
|
14282
|
+
{ signal: signalArg },
|
|
14283
|
+
);
|
|
14284
|
+
return;
|
|
14285
|
+
case 'forward':
|
|
14286
|
+
await lease.forward(
|
|
14287
|
+
operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
|
|
14288
|
+
{ signal: signalArg },
|
|
14289
|
+
);
|
|
14290
|
+
return;
|
|
14291
|
+
case 'reload':
|
|
14292
|
+
await lease.reload(
|
|
14293
|
+
operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
|
|
14294
|
+
{ signal: signalArg },
|
|
14295
|
+
);
|
|
14296
|
+
}
|
|
14297
|
+
}
|
|
14298
|
+
|
|
14299
|
+
private startBrowserAcknowledgement(
|
|
14300
|
+
viewArg: IControllerBrowserView,
|
|
14301
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14302
|
+
queuedArg: IControllerBrowserQueuedOperation,
|
|
14303
|
+
): void {
|
|
14304
|
+
const operationRequest = queuedArg.operation;
|
|
14305
|
+
if (operationRequest.type !== 'acknowledgeFrame') return;
|
|
14306
|
+
if (
|
|
14307
|
+
transportArg.activeAcknowledgements.size
|
|
14308
|
+
>= controllerBrowserTransportMaximumActiveAcknowledgements
|
|
14309
|
+
) {
|
|
14310
|
+
this.settleBrowserOperation(viewArg, transportArg, queuedArg, {
|
|
14311
|
+
status: 'failed',
|
|
14312
|
+
error: { code: 'operation_limit', message: 'Frame acknowledgement capacity was reached.' },
|
|
14313
|
+
});
|
|
12913
14314
|
return;
|
|
12914
14315
|
}
|
|
12915
|
-
const
|
|
12916
|
-
|
|
12917
|
-
:
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
14316
|
+
const acknowledgement: IControllerBrowserAcknowledgementOperation = {
|
|
14317
|
+
operationId: queuedArg.operationId,
|
|
14318
|
+
terminal: false,
|
|
14319
|
+
operation: Promise.resolve(),
|
|
14320
|
+
};
|
|
14321
|
+
transportArg.activeAcknowledgements.set(queuedArg.operationId, acknowledgement);
|
|
14322
|
+
const operation = (async () => {
|
|
14323
|
+
try {
|
|
14324
|
+
await this.assertBrowserViewAuthority(
|
|
14325
|
+
viewArg,
|
|
14326
|
+
transportArg.generation,
|
|
14327
|
+
transportArg.identity,
|
|
14328
|
+
transportArg.abortController.signal,
|
|
14329
|
+
);
|
|
14330
|
+
const accepted = await viewArg.handle.lease.acknowledgeFrame(
|
|
14331
|
+
operationRequest.acknowledgement,
|
|
14332
|
+
);
|
|
14333
|
+
if (!acknowledgement.terminal) {
|
|
14334
|
+
acknowledgement.terminal = true;
|
|
14335
|
+
this.queueBrowserOperationResult(viewArg, transportArg, queuedArg.operationId, {
|
|
14336
|
+
status: 'succeeded',
|
|
14337
|
+
accepted,
|
|
14338
|
+
});
|
|
14339
|
+
}
|
|
14340
|
+
} catch (errorArg) {
|
|
14341
|
+
if (!acknowledgement.terminal) {
|
|
14342
|
+
acknowledgement.terminal = true;
|
|
14343
|
+
this.queueBrowserOperationResult(viewArg, transportArg, queuedArg.operationId, {
|
|
14344
|
+
status: 'failed',
|
|
14345
|
+
error: this.browserOperationError(errorArg),
|
|
14346
|
+
});
|
|
14347
|
+
}
|
|
14348
|
+
} finally {
|
|
14349
|
+
transportArg.activeAcknowledgements.delete(queuedArg.operationId);
|
|
14350
|
+
}
|
|
14351
|
+
})();
|
|
14352
|
+
acknowledgement.operation = operation;
|
|
14353
|
+
}
|
|
14354
|
+
|
|
14355
|
+
private settleBrowserOperation(
|
|
14356
|
+
viewArg: IControllerBrowserView,
|
|
14357
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14358
|
+
queuedArg: IControllerBrowserQueuedOperation,
|
|
14359
|
+
resultArg: IControllerBrowserOperationResultPacket['result'],
|
|
14360
|
+
): void {
|
|
14361
|
+
if (queuedArg.terminal) return;
|
|
14362
|
+
queuedArg.terminal = true;
|
|
14363
|
+
this.queueBrowserOperationResult(viewArg, transportArg, queuedArg.operationId, resultArg);
|
|
14364
|
+
}
|
|
14365
|
+
|
|
14366
|
+
private queueBrowserOperationResult(
|
|
14367
|
+
viewArg: IControllerBrowserView,
|
|
14368
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14369
|
+
operationIdArg: number,
|
|
14370
|
+
resultArg: IControllerBrowserOperationResultPacket['result'],
|
|
14371
|
+
): void {
|
|
14372
|
+
transportArg.cancellableOperationIds.delete(operationIdArg);
|
|
14373
|
+
try {
|
|
14374
|
+
this.queueBrowserServerPacket(viewArg, transportArg, {
|
|
14375
|
+
protocol: controllerBrowserTransportProtocol,
|
|
14376
|
+
streamGeneration: transportArg.generation,
|
|
14377
|
+
type: 'operationResult',
|
|
14378
|
+
operationId: operationIdArg,
|
|
14379
|
+
result: resultArg,
|
|
14380
|
+
});
|
|
14381
|
+
} catch (errorArg) {
|
|
14382
|
+
this.noteBrowserTransportFault(viewArg, transportArg, errorArg, false);
|
|
14383
|
+
}
|
|
14384
|
+
}
|
|
14385
|
+
|
|
14386
|
+
private queueBrowserServerPacket(
|
|
14387
|
+
viewArg: IControllerBrowserView,
|
|
14388
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14389
|
+
packetArg: TControllerBrowserServerPacket,
|
|
14390
|
+
rawFrameArg?: Uint8Array,
|
|
14391
|
+
): Promise<void> | undefined {
|
|
14392
|
+
if (
|
|
14393
|
+
viewArg.closing
|
|
14394
|
+
|| viewArg.transport !== transportArg
|
|
14395
|
+
|| !this.browserTransportCanDeliverPacket(transportArg, packetArg)
|
|
14396
|
+
) return undefined;
|
|
14397
|
+
const packetBytes = encodeControllerBrowserTransportPacket(packetArg);
|
|
14398
|
+
const eventBytes = packetBytes.byteLength + (rawFrameArg?.byteLength ?? 0);
|
|
14399
|
+
if (
|
|
14400
|
+
transportArg.pendingEventCount >= controllerBrowserTransportMaximumPendingEvents
|
|
14401
|
+
|| transportArg.pendingEventBytes + eventBytes
|
|
14402
|
+
> controllerBrowserTransportMaximumPendingEventBytes
|
|
14403
|
+
) throw new Error('The browser view delivery queue is full.');
|
|
14404
|
+
transportArg.pendingEventCount += 1;
|
|
14405
|
+
transportArg.pendingEventBytes += eventBytes;
|
|
14406
|
+
const delivery = transportArg.deliveryTail.then(async () => {
|
|
14407
|
+
if (
|
|
14408
|
+
viewArg.closing
|
|
14409
|
+
|| viewArg.transport !== transportArg
|
|
14410
|
+
|| !this.browserTransportCanDeliverPacket(transportArg, packetArg)
|
|
14411
|
+
) return;
|
|
14412
|
+
await transportArg.events.send(packetBytes);
|
|
14413
|
+
if (rawFrameArg) await transportArg.events.send(rawFrameArg);
|
|
14414
|
+
});
|
|
14415
|
+
transportArg.deliveryTail = delivery.then(() => undefined, () => undefined);
|
|
14416
|
+
void delivery.catch((errorArg) => {
|
|
14417
|
+
this.noteBrowserTransportFault(viewArg, transportArg, errorArg, true);
|
|
12925
14418
|
}).finally(() => {
|
|
12926
|
-
|
|
12927
|
-
|
|
14419
|
+
transportArg.pendingEventCount -= 1;
|
|
14420
|
+
transportArg.pendingEventBytes -= eventBytes;
|
|
12928
14421
|
});
|
|
12929
|
-
|
|
14422
|
+
return delivery;
|
|
14423
|
+
}
|
|
14424
|
+
|
|
14425
|
+
private browserTransportCanDeliverPacket(
|
|
14426
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14427
|
+
packetArg: TControllerBrowserServerPacket,
|
|
14428
|
+
): boolean {
|
|
14429
|
+
return transportArg.lifecycle === 'active'
|
|
14430
|
+
|| (transportArg.lifecycle === 'ready' && packetArg.type === 'operationResult');
|
|
12930
14431
|
}
|
|
12931
14432
|
|
|
12932
|
-
private
|
|
14433
|
+
private failBrowserTransportProtocol(
|
|
12933
14434
|
viewArg: IControllerBrowserView,
|
|
12934
|
-
|
|
14435
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14436
|
+
reasonArg: unknown,
|
|
14437
|
+
): void {
|
|
14438
|
+
if (transportArg.lifecycle === 'retiring' || transportArg.lifecycle === 'retired') return;
|
|
14439
|
+
transportArg.lifecycle = 'failed';
|
|
14440
|
+
transportArg.failure = reasonArg;
|
|
14441
|
+
this.terminalizeBrowserTransportOperations(transportArg, reasonArg);
|
|
14442
|
+
void transportArg.events.abort(reasonArg).catch(() => undefined);
|
|
14443
|
+
void transportArg.operations.abort(reasonArg).catch(() => undefined);
|
|
14444
|
+
if (viewArg.transport !== transportArg) return;
|
|
14445
|
+
this.scheduleBrowserViewClose(viewArg, reasonArg);
|
|
14446
|
+
}
|
|
14447
|
+
|
|
14448
|
+
private noteBrowserTransportFault(
|
|
14449
|
+
viewArg: IControllerBrowserView,
|
|
14450
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14451
|
+
reasonArg: unknown,
|
|
14452
|
+
recoverableArg: boolean,
|
|
14453
|
+
): void {
|
|
14454
|
+
if (
|
|
14455
|
+
transportArg.lifecycle === 'failed'
|
|
14456
|
+
|| transportArg.lifecycle === 'retiring'
|
|
14457
|
+
|| transportArg.lifecycle === 'retired'
|
|
14458
|
+
) return;
|
|
14459
|
+
transportArg.lifecycle = 'failed';
|
|
14460
|
+
transportArg.failure = reasonArg;
|
|
14461
|
+
this.terminalizeBrowserTransportOperations(transportArg, reasonArg);
|
|
14462
|
+
void transportArg.events.abort(reasonArg).catch(() => undefined);
|
|
14463
|
+
void transportArg.operations.abort(reasonArg).catch(() => undefined);
|
|
14464
|
+
if (viewArg.transport !== transportArg) return;
|
|
14465
|
+
if (
|
|
14466
|
+
recoverableArg
|
|
14467
|
+
&& !viewArg.closing
|
|
14468
|
+
&& viewArg.transport === transportArg
|
|
14469
|
+
&& viewArg.lifecycle === 'active'
|
|
14470
|
+
) {
|
|
14471
|
+
viewArg.lifecycle = 'recoverable';
|
|
14472
|
+
viewArg.recoveryDeadlineAt = Date.now() + controllerBrowserTransportRecoveryGraceMs;
|
|
14473
|
+
this.armBrowserViewRecoveryDeadline(viewArg);
|
|
14474
|
+
return;
|
|
14475
|
+
}
|
|
14476
|
+
this.scheduleBrowserViewClose(viewArg, reasonArg);
|
|
14477
|
+
}
|
|
14478
|
+
|
|
14479
|
+
private terminalizeBrowserTransportOperations(
|
|
14480
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14481
|
+
reasonArg: unknown,
|
|
14482
|
+
): void {
|
|
14483
|
+
if (!transportArg.abortController.signal.aborted) {
|
|
14484
|
+
transportArg.abortController.abort(reasonArg);
|
|
14485
|
+
}
|
|
14486
|
+
for (const queued of transportArg.operationQueue.splice(0)) queued.terminal = true;
|
|
14487
|
+
transportArg.queuedOperationBytes = 0;
|
|
14488
|
+
transportArg.cancellableOperationIds.clear();
|
|
14489
|
+
if (transportArg.activeMutation) {
|
|
14490
|
+
transportArg.activeMutation.cancelled = true;
|
|
14491
|
+
transportArg.activeMutation.abortController?.abort(reasonArg);
|
|
14492
|
+
}
|
|
14493
|
+
for (const acknowledgement of transportArg.activeAcknowledgements.values()) {
|
|
14494
|
+
acknowledgement.terminal = true;
|
|
14495
|
+
}
|
|
14496
|
+
}
|
|
14497
|
+
|
|
14498
|
+
private armBrowserViewRecoveryDeadline(viewArg: IControllerBrowserView): void {
|
|
14499
|
+
if (viewArg.recoveryTimer || viewArg.recoveryDeadlineAt === undefined) return;
|
|
14500
|
+
const delayMs = Math.max(0, viewArg.recoveryDeadlineAt - Date.now());
|
|
14501
|
+
const timer = setTimeout(() => {
|
|
14502
|
+
if (viewArg.recoveryTimer === timer) viewArg.recoveryTimer = undefined;
|
|
14503
|
+
if (viewArg.closing || viewArg.lifecycle === 'active') return;
|
|
14504
|
+
this.scheduleBrowserViewClose(viewArg, new Error('Browser view recovery timed out.'));
|
|
14505
|
+
}, delayMs);
|
|
14506
|
+
viewArg.recoveryTimer = timer;
|
|
14507
|
+
timer.unref?.();
|
|
14508
|
+
}
|
|
14509
|
+
|
|
14510
|
+
private retireBrowserViewTransport(
|
|
14511
|
+
viewArg: IControllerBrowserView,
|
|
14512
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14513
|
+
deadlineAtArg: number,
|
|
12935
14514
|
): Promise<void> {
|
|
12936
|
-
|
|
12937
|
-
if (
|
|
12938
|
-
if (
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
|
|
14515
|
+
if (transportArg.lifecycle === 'retired') return Promise.resolve();
|
|
14516
|
+
if (transportArg.retirementTask) return transportArg.retirementTask;
|
|
14517
|
+
if (transportArg.retirementSettlement && !transportArg.retirementSettled) {
|
|
14518
|
+
return Promise.reject(new Error('Browser stream generation cleanup remains pending.'));
|
|
14519
|
+
}
|
|
14520
|
+
if (!transportArg.retirementSettlement) {
|
|
14521
|
+
transportArg.lifecycle = 'retiring';
|
|
14522
|
+
const retirementReason = new Error('The browser stream generation was retired.');
|
|
14523
|
+
this.terminalizeBrowserTransportOperations(transportArg, retirementReason);
|
|
14524
|
+
const settlement = Promise.allSettled([
|
|
14525
|
+
Promise.resolve().then(() => transportArg.events.abort(retirementReason)),
|
|
14526
|
+
Promise.resolve().then(() => transportArg.operations.abort(retirementReason)),
|
|
14527
|
+
transportArg.openingTask,
|
|
14528
|
+
transportArg.receiveTask,
|
|
14529
|
+
transportArg.drainTask,
|
|
14530
|
+
transportArg.deliveryTail,
|
|
14531
|
+
...[...transportArg.activeAcknowledgements.values()].map((entryArg) => entryArg.operation),
|
|
14532
|
+
].filter((candidateArg): candidateArg is Promise<void> => candidateArg !== undefined))
|
|
14533
|
+
.then(() => {
|
|
14534
|
+
transportArg.retirementSettled = true;
|
|
14535
|
+
transportArg.lifecycle = 'retired';
|
|
14536
|
+
const stillAssociated = viewArg.transport === transportArg;
|
|
14537
|
+
const slot = this.browserViewSlots.get(viewArg.peerId);
|
|
14538
|
+
if (stillAssociated && viewArg.closing) viewArg.transport = undefined;
|
|
14539
|
+
if (
|
|
14540
|
+
transportArg.retirementDeadlineExpired
|
|
14541
|
+
&& stillAssociated
|
|
14542
|
+
&& viewArg.closing
|
|
14543
|
+
&& slot?.view === viewArg
|
|
14544
|
+
) {
|
|
14545
|
+
void this.requestBrowserViewCleanup(slot, viewArg, retirementReason).catch((errorArg) => {
|
|
14546
|
+
console.error(`Browser view ${viewArg.id} late cleanup failed.`, errorArg);
|
|
14547
|
+
});
|
|
12943
14548
|
}
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
state: this.toControllerBrowserViewState(eventArg.state),
|
|
12947
|
-
};
|
|
12948
|
-
await typedSocket.createTypedRequest<IReq_ControllerBrowserViewEvent>(
|
|
12949
|
-
'controller.browser.view.event',
|
|
12950
|
-
viewArg.transportConnection,
|
|
12951
|
-
{ timeoutMs: 5_000 },
|
|
12952
|
-
).fire({
|
|
12953
|
-
viewId: viewArg.id,
|
|
12954
|
-
resourceId: viewArg.resource.id,
|
|
12955
|
-
attachmentRevision: viewArg.resource.attachmentRevision,
|
|
12956
|
-
event,
|
|
12957
|
-
}, { timeoutMs: 5_000, maxRetries: 0 });
|
|
12958
|
-
return;
|
|
14549
|
+
});
|
|
14550
|
+
transportArg.retirementSettlement = settlement;
|
|
12959
14551
|
}
|
|
12960
|
-
|
|
12961
|
-
const
|
|
12962
|
-
|
|
12963
|
-
|
|
12964
|
-
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
marker: browserFrameStreamAuthorizationMarker,
|
|
12968
|
-
view: viewArg,
|
|
12969
|
-
authority: viewArg.authority,
|
|
12970
|
-
transportPeer: viewArg.transportPeer,
|
|
12971
|
-
contentType: frame.mimeType,
|
|
12972
|
-
integrity,
|
|
14552
|
+
if (transportArg.retirementSettled) return Promise.resolve();
|
|
14553
|
+
const retirement = this.waitForBrowserTransportSettlement(
|
|
14554
|
+
transportArg.retirementSettlement,
|
|
14555
|
+
deadlineAtArg,
|
|
14556
|
+
).catch((errorArg) => {
|
|
14557
|
+
transportArg.retirementDeadlineExpired = true;
|
|
14558
|
+
throw errorArg;
|
|
12973
14559
|
});
|
|
12974
|
-
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
contentType: frame.mimeType,
|
|
12978
|
-
integrity,
|
|
12979
|
-
authorization,
|
|
14560
|
+
let tracked!: Promise<void>;
|
|
14561
|
+
tracked = retirement.finally(() => {
|
|
14562
|
+
if (transportArg.retirementTask === tracked) transportArg.retirementTask = undefined;
|
|
12980
14563
|
});
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
).
|
|
12991
|
-
|
|
12992
|
-
resourceId: viewArg.resource.id,
|
|
12993
|
-
attachmentRevision: viewArg.resource.attachmentRevision,
|
|
12994
|
-
event,
|
|
12995
|
-
}, { timeoutMs: 5_000, maxRetries: 0 });
|
|
12996
|
-
const production = (async () => {
|
|
12997
|
-
await stream.send(data);
|
|
12998
|
-
await stream.close();
|
|
12999
|
-
})();
|
|
14564
|
+
transportArg.retirementTask = tracked;
|
|
14565
|
+
return tracked;
|
|
14566
|
+
}
|
|
14567
|
+
|
|
14568
|
+
private async waitForBrowserTransportSettlement(
|
|
14569
|
+
settlementArg: Promise<void>,
|
|
14570
|
+
deadlineAtArg: number,
|
|
14571
|
+
): Promise<void> {
|
|
14572
|
+
const remainingMs = deadlineAtArg - Date.now();
|
|
14573
|
+
if (remainingMs <= 0) throw new Error('Browser stream generation cleanup timed out.');
|
|
14574
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
13000
14575
|
try {
|
|
13001
|
-
await Promise.
|
|
13002
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
14576
|
+
await Promise.race([
|
|
14577
|
+
settlementArg,
|
|
14578
|
+
new Promise<never>((_resolve, reject) => {
|
|
14579
|
+
timeout = setTimeout(
|
|
14580
|
+
() => reject(new Error('Browser stream generation cleanup timed out.')),
|
|
14581
|
+
remainingMs,
|
|
14582
|
+
);
|
|
14583
|
+
timeout.unref?.();
|
|
14584
|
+
}),
|
|
14585
|
+
]);
|
|
13006
14586
|
} finally {
|
|
13007
|
-
|
|
14587
|
+
if (timeout) clearTimeout(timeout);
|
|
13008
14588
|
}
|
|
13009
14589
|
}
|
|
13010
14590
|
|
|
13011
|
-
private
|
|
14591
|
+
private browserOperationError(errorArg: unknown): { code: string; message: string } {
|
|
14592
|
+
const typedError = asTypedResponseError(errorArg);
|
|
14593
|
+
const code = typeof typedError.errorData?.code === 'string'
|
|
14594
|
+
? typedError.errorData.code.slice(0, 128)
|
|
14595
|
+
: 'browser_operation_failed';
|
|
14596
|
+
return { code, message: typedError.message.slice(0, 512) };
|
|
14597
|
+
}
|
|
14598
|
+
|
|
14599
|
+
private bindBrowserStreamAuthorization(
|
|
13012
14600
|
authorizationArg: unknown,
|
|
13013
14601
|
contextArg: Readonly<plugins.typedsocket.IVirtualStreamAuthorizationContext>,
|
|
13014
14602
|
): plugins.typedsocket.IVirtualStreamAuthorizationBinding {
|
|
13015
14603
|
if (typeof authorizationArg !== 'object' || authorizationArg === null) {
|
|
13016
|
-
throw new Error('Unknown browser
|
|
14604
|
+
throw new Error('Unknown browser stream authority.');
|
|
13017
14605
|
}
|
|
13018
|
-
const authorization = authorizationArg as Partial<
|
|
14606
|
+
const authorization = authorizationArg as Partial<IControllerBrowserStreamAuthorization>;
|
|
13019
14607
|
const view = authorization.view;
|
|
13020
14608
|
if (
|
|
13021
|
-
authorization.marker !==
|
|
14609
|
+
authorization.marker !== browserStreamAuthorizationMarker
|
|
13022
14610
|
|| !view
|
|
13023
14611
|
|| authorization.authority !== view.authority
|
|
13024
14612
|
|| authorization.transportPeer !== view.transportPeer
|
|
14613
|
+
|| typeof authorization.transportIdentity !== 'symbol'
|
|
13025
14614
|
|| contextArg.target !== view.transportPeer
|
|
13026
|
-
||
|
|
14615
|
+
|| authorization.streamGeneration !== view.streamGeneration
|
|
14616
|
+
|| contextArg.creatorDirection !== authorization.creatorDirection
|
|
13027
14617
|
|| contextArg.contentType !== authorization.contentType
|
|
13028
|
-
|| contextArg.integrity
|
|
13029
|
-
|| contextArg.integrity?.byteLength !== authorization.integrity?.byteLength
|
|
13030
|
-
|| contextArg.integrity?.digest !== authorization.integrity?.digest
|
|
14618
|
+
|| contextArg.integrity !== undefined
|
|
13031
14619
|
|| view.closing
|
|
13032
|
-
|| view.activationState === 'inactive'
|
|
13033
14620
|
|| this.browserViews.get(view.id) !== view
|
|
13034
14621
|
|| this.browserViewSlots.get(view.peerId)?.view !== view
|
|
13035
|
-
|| !this.
|
|
13036
|
-
|
|
13037
|
-
|
|
14622
|
+
|| !this.isBrowserStreamBindingAuthorityCurrent(
|
|
14623
|
+
view,
|
|
14624
|
+
authorization.streamGeneration,
|
|
14625
|
+
authorization.transportIdentity,
|
|
14626
|
+
)
|
|
14627
|
+
) throw new Error('The browser stream authority is unavailable.');
|
|
14628
|
+
const bound = authorization as IControllerBrowserStreamAuthorization;
|
|
13038
14629
|
return {
|
|
13039
|
-
revalidate: (revalidationArg) => this.
|
|
14630
|
+
revalidate: (revalidationArg) => this.revalidateBrowserStream(
|
|
13040
14631
|
bound,
|
|
13041
14632
|
revalidationArg,
|
|
13042
14633
|
),
|
|
13043
14634
|
};
|
|
13044
14635
|
}
|
|
13045
14636
|
|
|
13046
|
-
private async
|
|
13047
|
-
authorizationArg:
|
|
14637
|
+
private async revalidateBrowserStream(
|
|
14638
|
+
authorizationArg: IControllerBrowserStreamAuthorization,
|
|
13048
14639
|
contextArg: Readonly<plugins.typedsocket.IVirtualStreamRevalidationContext>,
|
|
13049
14640
|
): Promise<boolean> {
|
|
13050
14641
|
const view = authorizationArg.view;
|
|
@@ -13054,32 +14645,93 @@ export class OpenCodeController {
|
|
|
13054
14645
|
|| contextArg.connection.peer !== authorizationArg.transportPeer
|
|
13055
14646
|
|| view.transportPeer !== authorizationArg.transportPeer
|
|
13056
14647
|
|| view.authority !== authorizationArg.authority
|
|
13057
|
-
|| view.
|
|
13058
|
-
||
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
14648
|
+
|| view.streamGeneration !== authorizationArg.streamGeneration
|
|
14649
|
+
|| !this.isBrowserViewInMemoryAuthorityCurrent(
|
|
14650
|
+
view,
|
|
14651
|
+
authorizationArg.streamGeneration,
|
|
14652
|
+
authorizationArg.transportIdentity,
|
|
14653
|
+
)
|
|
13063
14654
|
|| view.transportPeer.context.headers.get('origin') !== this.requireRuntimeConfig().publicOrigin
|
|
13064
|
-
|| !this.isBrowserRuntimeAuthorityCurrent(view)
|
|
13065
14655
|
) return false;
|
|
13066
14656
|
try {
|
|
13067
|
-
|
|
13068
|
-
view
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
13074
|
-
&& current.pendingAttachment === undefined
|
|
13075
|
-
&& current.attachmentAuthorityId === view.resource.attachmentAuthorityId
|
|
13076
|
-
&& current.attachmentRevision === view.resource.attachmentRevision
|
|
13077
|
-
&& this.isBrowserRuntimeAuthorityCurrent(view);
|
|
14657
|
+
await this.assertBrowserViewAuthority(
|
|
14658
|
+
view,
|
|
14659
|
+
authorizationArg.streamGeneration,
|
|
14660
|
+
authorizationArg.transportIdentity,
|
|
14661
|
+
contextArg.abortSignal,
|
|
14662
|
+
);
|
|
14663
|
+
return true;
|
|
13078
14664
|
} catch {
|
|
13079
14665
|
return false;
|
|
13080
14666
|
}
|
|
13081
14667
|
}
|
|
13082
14668
|
|
|
14669
|
+
private isBrowserStreamBindingAuthorityCurrent(
|
|
14670
|
+
viewArg: IControllerBrowserView,
|
|
14671
|
+
streamGenerationArg: number,
|
|
14672
|
+
transportIdentityArg: symbol,
|
|
14673
|
+
): boolean {
|
|
14674
|
+
return !viewArg.closing
|
|
14675
|
+
&& viewArg.lifecycle !== 'closing'
|
|
14676
|
+
&& viewArg.streamGeneration === streamGenerationArg
|
|
14677
|
+
&& viewArg.pendingTransportIdentity === transportIdentityArg
|
|
14678
|
+
&& this.browserViews.get(viewArg.id) === viewArg
|
|
14679
|
+
&& this.browserViewSlots.get(viewArg.peerId)?.view === viewArg
|
|
14680
|
+
&& this.authenticatedPeerIds.has(viewArg.peerId)
|
|
14681
|
+
&& viewArg.transportPeer.context.state[authenticatedPeerStateKey] === viewArg.credentialId
|
|
14682
|
+
&& this.isBrowserRuntimeAuthorityCurrent(viewArg);
|
|
14683
|
+
}
|
|
14684
|
+
|
|
14685
|
+
private isBrowserViewInMemoryAuthorityCurrent(
|
|
14686
|
+
viewArg: IControllerBrowserView,
|
|
14687
|
+
streamGenerationArg: number,
|
|
14688
|
+
transportIdentityArg: symbol,
|
|
14689
|
+
): boolean {
|
|
14690
|
+
return !viewArg.closing
|
|
14691
|
+
&& viewArg.lifecycle !== 'closing'
|
|
14692
|
+
&& viewArg.streamGeneration === streamGenerationArg
|
|
14693
|
+
&& viewArg.transport?.identity === transportIdentityArg
|
|
14694
|
+
&& this.browserViews.get(viewArg.id) === viewArg
|
|
14695
|
+
&& this.browserViewSlots.get(viewArg.peerId)?.view === viewArg
|
|
14696
|
+
&& this.authenticatedPeerIds.has(viewArg.peerId)
|
|
14697
|
+
&& viewArg.transportPeer.context.state[authenticatedPeerStateKey] === viewArg.credentialId
|
|
14698
|
+
&& this.isBrowserRuntimeAuthorityCurrent(viewArg);
|
|
14699
|
+
}
|
|
14700
|
+
|
|
14701
|
+
private async assertBrowserViewAuthority(
|
|
14702
|
+
viewArg: IControllerBrowserView,
|
|
14703
|
+
streamGenerationArg: number,
|
|
14704
|
+
transportIdentityArg: symbol,
|
|
14705
|
+
signalArg: AbortSignal,
|
|
14706
|
+
): Promise<void> {
|
|
14707
|
+
signalArg.throwIfAborted();
|
|
14708
|
+
if (!this.isBrowserViewInMemoryAuthorityCurrent(
|
|
14709
|
+
viewArg,
|
|
14710
|
+
streamGenerationArg,
|
|
14711
|
+
transportIdentityArg,
|
|
14712
|
+
)) {
|
|
14713
|
+
throw new ControllerResourceUnavailableError('The browser view authority is unavailable.');
|
|
14714
|
+
}
|
|
14715
|
+
const current = await awaitWithAbortSignal(this.requireAuthStore().getResource(
|
|
14716
|
+
viewArg.resource.projectId,
|
|
14717
|
+
viewArg.resource.id,
|
|
14718
|
+
), signalArg);
|
|
14719
|
+
signalArg.throwIfAborted();
|
|
14720
|
+
if (
|
|
14721
|
+
!current
|
|
14722
|
+
|| current.kind !== 'browser'
|
|
14723
|
+
|| current.lifecycle !== 'active'
|
|
14724
|
+
|| current.pendingAttachment
|
|
14725
|
+
|| current.attachmentAuthorityId !== viewArg.resource.attachmentAuthorityId
|
|
14726
|
+
|| current.attachmentRevision !== viewArg.resource.attachmentRevision
|
|
14727
|
+
|| !this.isBrowserViewInMemoryAuthorityCurrent(
|
|
14728
|
+
viewArg,
|
|
14729
|
+
streamGenerationArg,
|
|
14730
|
+
transportIdentityArg,
|
|
14731
|
+
)
|
|
14732
|
+
) throw new ControllerResourceUnavailableError('The browser resource changed.');
|
|
14733
|
+
}
|
|
14734
|
+
|
|
13083
14735
|
private isBrowserRuntimeAuthorityCurrent(viewArg: IControllerBrowserView): boolean {
|
|
13084
14736
|
try {
|
|
13085
14737
|
return viewArg.handle.lease.isAuthorityCurrent(viewArg.authority);
|
|
@@ -13088,11 +14740,14 @@ export class OpenCodeController {
|
|
|
13088
14740
|
}
|
|
13089
14741
|
}
|
|
13090
14742
|
|
|
13091
|
-
private
|
|
14743
|
+
private nextControllerBrowserViewState(
|
|
14744
|
+
viewArg: IControllerBrowserView,
|
|
13092
14745
|
stateArg: plugins.browserRuntime.IBrowserRuntimeState,
|
|
13093
|
-
):
|
|
14746
|
+
): IControllerBrowserViewState {
|
|
13094
14747
|
const { lastError, ...state } = stateArg;
|
|
14748
|
+
viewArg.stateRevision += 1;
|
|
13095
14749
|
return {
|
|
14750
|
+
revision: viewArg.stateRevision,
|
|
13096
14751
|
...state,
|
|
13097
14752
|
...(lastError === undefined ? {} : {
|
|
13098
14753
|
lastError: { ...lastError, message: lastError.code },
|