agl 23.0.0 → 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 +11 -0
- package/dist_serve/bundle.js +811 -808
- 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 +35 -5
- package/dist_ts/classes.controller.js +994 -229
- package/dist_ts/classes.validation.d.ts +9 -1
- package/dist_ts/classes.validation.js +62 -7
- 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 +10 -6
- package/dist_ts_interfaces/interfaces.js +2 -3
- package/dist_ts_interfaces/requests.d.ts +20 -21
- package/package.json +5 -5
- package/readme.md +7 -3
- 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 +1367 -279
- package/ts/classes.validation.ts +77 -6
- package/ts_interfaces/browsertransport.ts +126 -0
- package/ts_interfaces/index.ts +1 -0
- package/ts_interfaces/interfaces.ts +11 -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 +6 -17
- package/ts_web/elements.harnesscontrollerapp.ts +450 -115
- package/ts_web/index.ts +1 -0
- package/ts_web/plugins.ts +2 -0
package/ts/classes.controller.ts
CHANGED
|
@@ -12,6 +12,21 @@ 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,
|
|
15
30
|
controllerLiveMessageDeltaTargetBytes,
|
|
16
31
|
controllerInitialMessageBundleLimit,
|
|
17
32
|
controllerMaximumTranscriptBytes,
|
|
@@ -95,8 +110,15 @@ import {
|
|
|
95
110
|
type IReq_ControllerBrowserViewOpen,
|
|
96
111
|
type IReq_ControllerBrowserViewActivate,
|
|
97
112
|
type IReq_ControllerBrowserViewClose,
|
|
98
|
-
type
|
|
99
|
-
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,
|
|
100
122
|
type IReq_ControllerProjectCreate,
|
|
101
123
|
type IReq_ControllerProjectList,
|
|
102
124
|
type IReq_ControllerProjectRemove,
|
|
@@ -242,7 +264,8 @@ import {
|
|
|
242
264
|
assertBrowserViewOpenRequest,
|
|
243
265
|
assertBrowserViewActivateRequest,
|
|
244
266
|
assertBrowserViewCloseRequest,
|
|
245
|
-
|
|
267
|
+
assertBrowserViewRecoverRequest,
|
|
268
|
+
assertControllerBrowserClientPacket,
|
|
246
269
|
assertProjectCreateRequest,
|
|
247
270
|
resolveProjectPathInput,
|
|
248
271
|
assertProjectRemoveRequest,
|
|
@@ -332,9 +355,12 @@ const trustedProxySecretHeader = 'x-harness-controller-proxy-secret';
|
|
|
332
355
|
const websocketPayloadLimit = 16 * 1024 * 1024;
|
|
333
356
|
const browserViewActivationTimeoutMs = 15_000;
|
|
334
357
|
const browserViewCleanupRetryMs = 1_000;
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
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');
|
|
338
364
|
const browserSemanticOperationClassifications: ReadonlySet<
|
|
339
365
|
plugins.browserRuntime.TBrowserRuntimeOperationClassification
|
|
340
366
|
> = new Set(['navigation', 'tab', 'agent-action']);
|
|
@@ -476,22 +502,122 @@ interface ITerminalPeerLifecycleState {
|
|
|
476
502
|
connected: boolean;
|
|
477
503
|
}
|
|
478
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
|
+
|
|
479
594
|
interface IControllerBrowserView {
|
|
480
595
|
id: string;
|
|
481
596
|
peerId: string;
|
|
597
|
+
credentialId: string;
|
|
482
598
|
transportConnection: plugins.typedsocket.ISmartServeConnectionWrapper;
|
|
483
599
|
transportPeer: plugins.typedsocket.ISmartServeConnectionWrapper['peer'];
|
|
484
600
|
resource: IControllerResourceDocument;
|
|
485
601
|
handle: Awaited<ReturnType<ControllerBrowserResourceHost['openHumanView']>>;
|
|
486
602
|
authority: plugins.browserRuntime.TBrowserRuntimeLeaseAuthority;
|
|
487
|
-
|
|
603
|
+
lifecycle: TControllerBrowserViewLifecycle;
|
|
488
604
|
activationAbortController: AbortController;
|
|
489
605
|
activationTimer?: ReturnType<typeof setTimeout>;
|
|
490
606
|
activationPromise?: Promise<void>;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
+
};
|
|
495
621
|
closing: boolean;
|
|
496
622
|
cleanupTask?: Promise<void>;
|
|
497
623
|
cleanupRetryTimer?: ReturnType<typeof setTimeout>;
|
|
@@ -503,13 +629,15 @@ interface IControllerBrowserViewSlot {
|
|
|
503
629
|
view?: IControllerBrowserView;
|
|
504
630
|
}
|
|
505
631
|
|
|
506
|
-
interface
|
|
507
|
-
readonly marker: typeof
|
|
632
|
+
interface IControllerBrowserStreamAuthorization {
|
|
633
|
+
readonly marker: typeof browserStreamAuthorizationMarker;
|
|
508
634
|
readonly view: IControllerBrowserView;
|
|
509
635
|
readonly authority: plugins.browserRuntime.TBrowserRuntimeLeaseAuthority;
|
|
510
636
|
readonly transportPeer: plugins.typedsocket.ISmartServeConnectionWrapper['peer'];
|
|
637
|
+
readonly transportIdentity: symbol;
|
|
638
|
+
readonly streamGeneration: number;
|
|
639
|
+
readonly creatorDirection: plugins.typedrequestInterfaces.TVirtualStreamDirection;
|
|
511
640
|
readonly contentType: string;
|
|
512
|
-
readonly integrity: plugins.typedrequestInterfaces.IVirtualStreamIntegrity;
|
|
513
641
|
}
|
|
514
642
|
|
|
515
643
|
interface IFlexRefreshJobCacheEntry {
|
|
@@ -1611,6 +1739,11 @@ export class OpenCodeController {
|
|
|
1611
1739
|
private resourceCoordinator?: ControllerResourceCoordinator;
|
|
1612
1740
|
private readonly browserViews = new Map<string, IControllerBrowserView>();
|
|
1613
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
|
+
);
|
|
1614
1747
|
private readonly authenticatedPeerIds = new Set<string>();
|
|
1615
1748
|
private openCodeResourceGeneration = 0;
|
|
1616
1749
|
private flexResourceGeneration = 0;
|
|
@@ -2059,7 +2192,7 @@ export class OpenCodeController {
|
|
|
2059
2192
|
headersTimeout: 10_000,
|
|
2060
2193
|
requestTimeout: 30_000,
|
|
2061
2194
|
virtualStreamAuthorizationAdapter: {
|
|
2062
|
-
bind: (authorizationArg, contextArg) => this.
|
|
2195
|
+
bind: (authorizationArg, contextArg) => this.bindBrowserStreamAuthorization(
|
|
2063
2196
|
authorizationArg,
|
|
2064
2197
|
contextArg,
|
|
2065
2198
|
),
|
|
@@ -3046,6 +3179,7 @@ export class OpenCodeController {
|
|
|
3046
3179
|
(terminalPeerLifecycle as ITerminalPeerLifecycleState).connected = false;
|
|
3047
3180
|
}
|
|
3048
3181
|
this.authenticatedPeerIds.delete(peerArg.id);
|
|
3182
|
+
this.browserViewRealtimeBucketsByPeer.delete(peerArg.id);
|
|
3049
3183
|
const cleanupResults = await Promise.allSettled([
|
|
3050
3184
|
this.terminalManager?.detachPeer(peerArg.id) ?? Promise.resolve(),
|
|
3051
3185
|
this.closeBrowserViewsForPeer(peerArg.id),
|
|
@@ -3243,6 +3377,7 @@ export class OpenCodeController {
|
|
|
3243
3377
|
}
|
|
3244
3378
|
this.resourceCoordinator = undefined;
|
|
3245
3379
|
this.authenticatedPeerIds.clear();
|
|
3380
|
+
this.browserViewRealtimeBucketsByPeer.clear();
|
|
3246
3381
|
|
|
3247
3382
|
await Promise.allSettled([
|
|
3248
3383
|
...(this.openCodeControllerEventDrainTask ? [this.openCodeControllerEventDrainTask] : []),
|
|
@@ -5808,43 +5943,52 @@ export class OpenCodeController {
|
|
|
5808
5943
|
'controller.browser.view.activate',
|
|
5809
5944
|
async (requestArg, toolsArg) => {
|
|
5810
5945
|
const peer = peerFromLocalData(toolsArg?.localData);
|
|
5811
|
-
const { viewId } = validateRequest(
|
|
5946
|
+
const { viewId, streamGeneration } = validateRequest(
|
|
5947
|
+
() => assertBrowserViewActivateRequest(requestArg),
|
|
5948
|
+
);
|
|
5812
5949
|
return this.runOperation(peer, async (signalArg) => {
|
|
5813
|
-
await this.activateBrowserView(viewId, peer
|
|
5950
|
+
await this.activateBrowserView(viewId, peer, streamGeneration, signalArg);
|
|
5814
5951
|
return { activated: true as const };
|
|
5815
5952
|
}, toolsArg?.abortSignal);
|
|
5816
5953
|
},
|
|
5817
5954
|
));
|
|
5818
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
|
+
|
|
5819
5971
|
protectedRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_ControllerBrowserViewClose>(
|
|
5820
5972
|
'controller.browser.view.close',
|
|
5821
5973
|
async (requestArg, toolsArg) => {
|
|
5822
5974
|
const peer = peerFromLocalData(toolsArg?.localData);
|
|
5823
|
-
const { viewId } = validateRequest(
|
|
5975
|
+
const { viewId, expectedStreamGeneration } = validateRequest(
|
|
5976
|
+
() => assertBrowserViewCloseRequest(requestArg),
|
|
5977
|
+
);
|
|
5824
5978
|
return this.runOperation(peer, async () => this.runAudited(
|
|
5825
5979
|
{ type: 'resource.browser.view.close', peerId: peer.id },
|
|
5826
5980
|
async () => {
|
|
5827
|
-
await this.closeBrowserView(
|
|
5828
|
-
|
|
5981
|
+
const closed = await this.closeBrowserView(
|
|
5982
|
+
viewId,
|
|
5983
|
+
peer,
|
|
5984
|
+
expectedStreamGeneration,
|
|
5985
|
+
);
|
|
5986
|
+
return { closed };
|
|
5829
5987
|
},
|
|
5830
5988
|
));
|
|
5831
5989
|
},
|
|
5832
5990
|
));
|
|
5833
5991
|
|
|
5834
|
-
protectedRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_ControllerBrowserViewOperate>(
|
|
5835
|
-
'controller.browser.view.operate',
|
|
5836
|
-
async (requestArg, toolsArg) => {
|
|
5837
|
-
const peer = peerFromLocalData(toolsArg?.localData);
|
|
5838
|
-
const parsed = validateRequest(() => assertBrowserViewOperateRequest(requestArg));
|
|
5839
|
-
return this.runOperation(peer, async (signalArg) => this.operateBrowserView(
|
|
5840
|
-
peer.id,
|
|
5841
|
-
parsed.viewId,
|
|
5842
|
-
parsed.operation,
|
|
5843
|
-
signalArg,
|
|
5844
|
-
), toolsArg?.abortSignal);
|
|
5845
|
-
},
|
|
5846
|
-
));
|
|
5847
|
-
|
|
5848
5992
|
protectedRouter.addTypedHandler(
|
|
5849
5993
|
new plugins.typedrequest.TypedHandler<IReq_ControllerSessionGroupsGet>(
|
|
5850
5994
|
'controller.sessiongroups.get',
|
|
@@ -13018,7 +13162,9 @@ export class OpenCodeController {
|
|
|
13018
13162
|
transportConnectionArg: plugins.typedsocket.ISmartServeConnectionWrapper,
|
|
13019
13163
|
requestArg: IReq_ControllerBrowserViewOpen['request'],
|
|
13020
13164
|
signalArg: AbortSignal,
|
|
13021
|
-
): Promise<
|
|
13165
|
+
): Promise<plugins.typedrequest.TReverseVirtualStreamDirections<
|
|
13166
|
+
IReq_ControllerBrowserViewOpen['response']
|
|
13167
|
+
>> {
|
|
13022
13168
|
return this.runBrowserViewSlotOperation(peerArg.id, async (slotArg) => {
|
|
13023
13169
|
const transportPeer = transportConnectionArg.peer;
|
|
13024
13170
|
signalArg.throwIfAborted();
|
|
@@ -13075,17 +13221,17 @@ export class OpenCodeController {
|
|
|
13075
13221
|
view = {
|
|
13076
13222
|
id: viewId,
|
|
13077
13223
|
peerId: peerArg.id,
|
|
13224
|
+
credentialId,
|
|
13078
13225
|
transportConnection: transportConnectionArg,
|
|
13079
13226
|
transportPeer,
|
|
13080
13227
|
resource,
|
|
13081
13228
|
handle,
|
|
13082
13229
|
authority: handle.lease.getAuthority(),
|
|
13083
|
-
|
|
13230
|
+
lifecycle: 'inactive',
|
|
13084
13231
|
activationAbortController: new AbortController(),
|
|
13085
|
-
|
|
13086
|
-
|
|
13087
|
-
|
|
13088
|
-
activeFrameStreams: new Set(),
|
|
13232
|
+
streamGeneration: 1,
|
|
13233
|
+
stateRevision: 0,
|
|
13234
|
+
recoveryAttempts: 0,
|
|
13089
13235
|
closing: false,
|
|
13090
13236
|
};
|
|
13091
13237
|
slotArg.view = view;
|
|
@@ -13101,6 +13247,9 @@ export class OpenCodeController {
|
|
|
13101
13247
|
);
|
|
13102
13248
|
}
|
|
13103
13249
|
this.browserViews.set(view.id, view);
|
|
13250
|
+
const transport = await this.createBrowserViewTransport(view, 1);
|
|
13251
|
+
this.beginBrowserViewTransportOpening(view, transport);
|
|
13252
|
+
signalArg.throwIfAborted();
|
|
13104
13253
|
view.activationTimer = setTimeout(() => {
|
|
13105
13254
|
this.scheduleBrowserViewClose(view!, new Error('Browser view activation timed out.'));
|
|
13106
13255
|
}, browserViewActivationTimeoutMs);
|
|
@@ -13109,7 +13258,10 @@ export class OpenCodeController {
|
|
|
13109
13258
|
viewId,
|
|
13110
13259
|
resourceId: resource.id,
|
|
13111
13260
|
attachmentRevision: resource.attachmentRevision,
|
|
13112
|
-
|
|
13261
|
+
streamGeneration: transport.generation,
|
|
13262
|
+
events: transport.events,
|
|
13263
|
+
operations: transport.operations,
|
|
13264
|
+
state: this.nextControllerBrowserViewState(view, handle.lease.getState()),
|
|
13113
13265
|
};
|
|
13114
13266
|
} catch (errorArg) {
|
|
13115
13267
|
let cleanupError: unknown;
|
|
@@ -13135,43 +13287,53 @@ export class OpenCodeController {
|
|
|
13135
13287
|
|
|
13136
13288
|
private async activateBrowserView(
|
|
13137
13289
|
viewIdArg: string,
|
|
13138
|
-
|
|
13290
|
+
peerArg: IControllerPeer,
|
|
13291
|
+
streamGenerationArg: number,
|
|
13139
13292
|
signalArg: AbortSignal,
|
|
13140
13293
|
): Promise<void> {
|
|
13141
|
-
await this.runBrowserViewSlotOperation(
|
|
13294
|
+
await this.runBrowserViewSlotOperation(peerArg.id, async (slotArg) => {
|
|
13142
13295
|
const view = slotArg.view;
|
|
13296
|
+
const transport = view?.transport;
|
|
13143
13297
|
if (
|
|
13144
13298
|
!view
|
|
13145
13299
|
|| view.id !== viewIdArg
|
|
13146
13300
|
|| view.closing
|
|
13301
|
+
|| view.transportPeer !== peerArg
|
|
13302
|
+
|| view.credentialId !== peerArg.context.state[authenticatedPeerStateKey]
|
|
13303
|
+
|| view.streamGeneration !== streamGenerationArg
|
|
13304
|
+
|| !transport
|
|
13305
|
+
|| transport.generation !== streamGenerationArg
|
|
13147
13306
|
|| this.browserViews.get(viewIdArg) !== view
|
|
13148
13307
|
) {
|
|
13149
13308
|
throw new plugins.typedrequest.TypedResponseError('The browser view is unavailable.', {
|
|
13150
13309
|
code: 'resource_unavailable',
|
|
13151
13310
|
});
|
|
13152
13311
|
}
|
|
13153
|
-
|
|
13312
|
+
await this.assertBrowserViewAuthority(
|
|
13313
|
+
view,
|
|
13314
|
+
streamGenerationArg,
|
|
13315
|
+
transport.identity,
|
|
13316
|
+
signalArg,
|
|
13317
|
+
);
|
|
13318
|
+
if (view.lifecycle === 'active' && transport.lifecycle === 'active') return;
|
|
13154
13319
|
if (view.activationPromise) {
|
|
13155
13320
|
await awaitWithAbortSignal(view.activationPromise, signalArg);
|
|
13156
13321
|
return;
|
|
13157
13322
|
}
|
|
13158
|
-
view.
|
|
13159
|
-
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
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);
|
|
13163
13331
|
view.activationPromise = activation;
|
|
13164
|
-
const activationSignal = AbortSignal.any([
|
|
13165
|
-
signalArg,
|
|
13166
|
-
view.activationAbortController.signal,
|
|
13167
|
-
]);
|
|
13168
13332
|
try {
|
|
13169
|
-
await
|
|
13170
|
-
activationSignal.throwIfAborted();
|
|
13333
|
+
await activation;
|
|
13171
13334
|
if (view.closing || slotArg.view !== view) {
|
|
13172
13335
|
throw new Error('The browser view changed during activation.');
|
|
13173
13336
|
}
|
|
13174
|
-
view.activationState = 'active';
|
|
13175
13337
|
if (view.activationTimer) clearTimeout(view.activationTimer);
|
|
13176
13338
|
view.activationTimer = undefined;
|
|
13177
13339
|
} catch (errorArg) {
|
|
@@ -13183,11 +13345,173 @@ export class OpenCodeController {
|
|
|
13183
13345
|
});
|
|
13184
13346
|
}
|
|
13185
13347
|
|
|
13186
|
-
private async
|
|
13187
|
-
|
|
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);
|
|
13188
13502
|
const view = slot?.view;
|
|
13189
|
-
if (
|
|
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;
|
|
13190
13513
|
await this.requestBrowserViewCleanup(slot, view, new Error('Browser view closed.'));
|
|
13514
|
+
return true;
|
|
13191
13515
|
}
|
|
13192
13516
|
|
|
13193
13517
|
private async closeBrowserViewsForPeer(peerIdArg: string): Promise<void> {
|
|
@@ -13261,15 +13585,22 @@ export class OpenCodeController {
|
|
|
13261
13585
|
private markBrowserViewClosing(viewArg: IControllerBrowserView, reasonArg: unknown): void {
|
|
13262
13586
|
if (!viewArg.closing) {
|
|
13263
13587
|
viewArg.closing = true;
|
|
13588
|
+
viewArg.lifecycle = 'closing';
|
|
13264
13589
|
if (this.browserViews.get(viewArg.id) === viewArg) this.browserViews.delete(viewArg.id);
|
|
13265
13590
|
if (viewArg.activationTimer) clearTimeout(viewArg.activationTimer);
|
|
13266
13591
|
viewArg.activationTimer = undefined;
|
|
13592
|
+
if (viewArg.recoveryTimer) clearTimeout(viewArg.recoveryTimer);
|
|
13593
|
+
viewArg.recoveryTimer = undefined;
|
|
13267
13594
|
if (!viewArg.activationAbortController.signal.aborted) {
|
|
13268
13595
|
viewArg.activationAbortController.abort(reasonArg);
|
|
13269
13596
|
}
|
|
13270
13597
|
}
|
|
13271
|
-
|
|
13272
|
-
|
|
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);
|
|
13273
13604
|
}
|
|
13274
13605
|
}
|
|
13275
13606
|
|
|
@@ -13334,14 +13665,18 @@ export class OpenCodeController {
|
|
|
13334
13665
|
this.markBrowserViewClosing(viewArg, reasonArg);
|
|
13335
13666
|
if (slotArg.view !== viewArg) return;
|
|
13336
13667
|
const errors: unknown[] = [];
|
|
13337
|
-
const
|
|
13338
|
-
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
.
|
|
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
|
+
}
|
|
13343
13679
|
await viewArg.handle.close().catch((errorArg) => errors.push(errorArg));
|
|
13344
|
-
await viewArg.deliveryTail.catch((errorArg) => errors.push(errorArg));
|
|
13345
13680
|
if (errors.length === 1) throw errors[0];
|
|
13346
13681
|
if (errors.length > 1) {
|
|
13347
13682
|
throw new AggregateError(errors, 'Browser view cleanup is incomplete.');
|
|
@@ -13357,6 +13692,13 @@ export class OpenCodeController {
|
|
|
13357
13692
|
reasonArg: unknown,
|
|
13358
13693
|
): void {
|
|
13359
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;
|
|
13360
13702
|
const timer = setTimeout(() => {
|
|
13361
13703
|
if (viewArg.cleanupRetryTimer === timer) viewArg.cleanupRetryTimer = undefined;
|
|
13362
13704
|
if (slotArg.view !== viewArg) return;
|
|
@@ -13368,250 +13710,932 @@ export class OpenCodeController {
|
|
|
13368
13710
|
timer.unref?.();
|
|
13369
13711
|
}
|
|
13370
13712
|
|
|
13371
|
-
private
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
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),
|
|
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
|
+
}
|
|
13909
|
+
}
|
|
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);
|
|
13381
13951
|
});
|
|
13952
|
+
} finally {
|
|
13953
|
+
if (timeout) clearTimeout(timeout);
|
|
13382
13954
|
}
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
|
|
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
|
+
};
|
|
13387
14030
|
if (
|
|
13388
|
-
|
|
13389
|
-
||
|
|
13390
|
-
|
|
13391
|
-
|| current.pendingAttachment
|
|
13392
|
-
|| current.attachmentAuthorityId !== view.resource.attachmentAuthorityId
|
|
13393
|
-
|| current.attachmentRevision !== view.resource.attachmentRevision
|
|
14031
|
+
transportArg.operationQueue.length >= controllerBrowserTransportMaximumPendingOperations
|
|
14032
|
+
|| transportArg.queuedOperationBytes + byteLengthArg
|
|
14033
|
+
> controllerBrowserTransportMaximumQueuedOperationBytes
|
|
13394
14034
|
) {
|
|
13395
|
-
|
|
13396
|
-
|
|
13397
|
-
code: '
|
|
14035
|
+
this.settleBrowserOperation(viewArg, transportArg, queued, {
|
|
14036
|
+
status: 'failed',
|
|
14037
|
+
error: { code: 'operation_limit', message: 'The browser operation queue is full.' },
|
|
13398
14038
|
});
|
|
14039
|
+
return;
|
|
13399
14040
|
}
|
|
13400
|
-
|
|
13401
|
-
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
13417
|
-
|
|
13418
|
-
|
|
13419
|
-
await lease.dispatchKey(operationArg.input, { signal: signalArg });
|
|
13420
|
-
break;
|
|
13421
|
-
case 'insertText':
|
|
13422
|
-
await lease.insertText(operationArg.input, { signal: signalArg });
|
|
13423
|
-
break;
|
|
13424
|
-
case 'navigate':
|
|
13425
|
-
await lease.executeHumanAction({
|
|
13426
|
-
action: 'navigate',
|
|
13427
|
-
url: operationArg.url,
|
|
13428
|
-
...(operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId }),
|
|
13429
|
-
...(operationArg.timeoutMs === undefined ? {} : { timeoutMs: operationArg.timeoutMs }),
|
|
13430
|
-
}, { signal: signalArg });
|
|
13431
|
-
break;
|
|
13432
|
-
case 'createTab':
|
|
13433
|
-
await lease.createTab({
|
|
13434
|
-
...(operationArg.url === undefined ? {} : { url: operationArg.url }),
|
|
13435
|
-
...(operationArg.activate === undefined ? {} : { activate: operationArg.activate }),
|
|
13436
|
-
}, { signal: signalArg });
|
|
13437
|
-
break;
|
|
13438
|
-
case 'activateTab':
|
|
13439
|
-
await lease.activateTab(operationArg.tabId, { signal: signalArg });
|
|
13440
|
-
break;
|
|
13441
|
-
case 'closeTab':
|
|
13442
|
-
await lease.closeTab(operationArg.tabId, { signal: signalArg });
|
|
13443
|
-
break;
|
|
13444
|
-
case 'back':
|
|
13445
|
-
await lease.back(
|
|
13446
|
-
operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
|
|
13447
|
-
{ signal: signalArg },
|
|
13448
|
-
);
|
|
13449
|
-
break;
|
|
13450
|
-
case 'forward':
|
|
13451
|
-
await lease.forward(
|
|
13452
|
-
operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
|
|
13453
|
-
{ signal: signalArg },
|
|
13454
|
-
);
|
|
13455
|
-
break;
|
|
13456
|
-
case 'reload':
|
|
13457
|
-
await lease.reload(
|
|
13458
|
-
operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
|
|
13459
|
-
{ signal: signalArg },
|
|
13460
|
-
);
|
|
13461
|
-
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);
|
|
13462
14060
|
}
|
|
13463
|
-
} catch (errorArg) {
|
|
13464
|
-
throw asTypedResponseError(errorArg);
|
|
13465
14061
|
}
|
|
13466
|
-
return { accepted, state: this.toControllerBrowserViewState(lease.getState()) };
|
|
13467
14062
|
}
|
|
13468
14063
|
|
|
13469
|
-
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(
|
|
13470
14092
|
viewArg: IControllerBrowserView,
|
|
13471
|
-
|
|
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,
|
|
14126
|
+
): void {
|
|
14127
|
+
if (
|
|
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
|
|
14154
|
+
) {
|
|
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,
|
|
13472
14303
|
): void {
|
|
13473
|
-
const
|
|
14304
|
+
const operationRequest = queuedArg.operation;
|
|
14305
|
+
if (operationRequest.type !== 'acknowledgeFrame') return;
|
|
13474
14306
|
if (
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|| viewArg.pendingEventBytes + eventBytes > maxPendingBrowserViewEventBytes
|
|
14307
|
+
transportArg.activeAcknowledgements.size
|
|
14308
|
+
>= controllerBrowserTransportMaximumActiveAcknowledgements
|
|
13478
14309
|
) {
|
|
13479
|
-
this.
|
|
14310
|
+
this.settleBrowserOperation(viewArg, transportArg, queuedArg, {
|
|
14311
|
+
status: 'failed',
|
|
14312
|
+
error: { code: 'operation_limit', message: 'Frame acknowledgement capacity was reached.' },
|
|
14313
|
+
});
|
|
13480
14314
|
return;
|
|
13481
14315
|
}
|
|
13482
|
-
const
|
|
13483
|
-
|
|
13484
|
-
:
|
|
13485
|
-
|
|
13486
|
-
|
|
13487
|
-
|
|
13488
|
-
|
|
13489
|
-
|
|
13490
|
-
|
|
13491
|
-
|
|
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);
|
|
13492
14418
|
}).finally(() => {
|
|
13493
|
-
|
|
13494
|
-
|
|
14419
|
+
transportArg.pendingEventCount -= 1;
|
|
14420
|
+
transportArg.pendingEventBytes -= eventBytes;
|
|
13495
14421
|
});
|
|
13496
|
-
|
|
14422
|
+
return delivery;
|
|
13497
14423
|
}
|
|
13498
14424
|
|
|
13499
|
-
private
|
|
14425
|
+
private browserTransportCanDeliverPacket(
|
|
14426
|
+
transportArg: IControllerBrowserViewTransport,
|
|
14427
|
+
packetArg: TControllerBrowserServerPacket,
|
|
14428
|
+
): boolean {
|
|
14429
|
+
return transportArg.lifecycle === 'active'
|
|
14430
|
+
|| (transportArg.lifecycle === 'ready' && packetArg.type === 'operationResult');
|
|
14431
|
+
}
|
|
14432
|
+
|
|
14433
|
+
private failBrowserTransportProtocol(
|
|
13500
14434
|
viewArg: IControllerBrowserView,
|
|
13501
|
-
|
|
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,
|
|
13502
14514
|
): Promise<void> {
|
|
13503
|
-
|
|
13504
|
-
if (
|
|
13505
|
-
if (
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
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
|
+
});
|
|
13510
14548
|
}
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
state: this.toControllerBrowserViewState(eventArg.state),
|
|
13514
|
-
};
|
|
13515
|
-
await typedSocket.createTypedRequest<IReq_ControllerBrowserViewEvent>(
|
|
13516
|
-
'controller.browser.view.event',
|
|
13517
|
-
viewArg.transportConnection,
|
|
13518
|
-
{ timeoutMs: 5_000 },
|
|
13519
|
-
).fire({
|
|
13520
|
-
viewId: viewArg.id,
|
|
13521
|
-
resourceId: viewArg.resource.id,
|
|
13522
|
-
attachmentRevision: viewArg.resource.attachmentRevision,
|
|
13523
|
-
event,
|
|
13524
|
-
}, { timeoutMs: 5_000, maxRetries: 0 });
|
|
13525
|
-
return;
|
|
14549
|
+
});
|
|
14550
|
+
transportArg.retirementSettlement = settlement;
|
|
13526
14551
|
}
|
|
13527
|
-
|
|
13528
|
-
const
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
13533
|
-
|
|
13534
|
-
marker: browserFrameStreamAuthorizationMarker,
|
|
13535
|
-
view: viewArg,
|
|
13536
|
-
authority: viewArg.authority,
|
|
13537
|
-
transportPeer: viewArg.transportPeer,
|
|
13538
|
-
contentType: frame.mimeType,
|
|
13539
|
-
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;
|
|
13540
14559
|
});
|
|
13541
|
-
|
|
13542
|
-
|
|
13543
|
-
|
|
13544
|
-
contentType: frame.mimeType,
|
|
13545
|
-
integrity,
|
|
13546
|
-
authorization,
|
|
14560
|
+
let tracked!: Promise<void>;
|
|
14561
|
+
tracked = retirement.finally(() => {
|
|
14562
|
+
if (transportArg.retirementTask === tracked) transportArg.retirementTask = undefined;
|
|
13547
14563
|
});
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
).
|
|
13558
|
-
|
|
13559
|
-
resourceId: viewArg.resource.id,
|
|
13560
|
-
attachmentRevision: viewArg.resource.attachmentRevision,
|
|
13561
|
-
event,
|
|
13562
|
-
}, { timeoutMs: 5_000, maxRetries: 0 });
|
|
13563
|
-
const production = (async () => {
|
|
13564
|
-
await stream.send(data);
|
|
13565
|
-
await stream.close();
|
|
13566
|
-
})();
|
|
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;
|
|
13567
14575
|
try {
|
|
13568
|
-
await Promise.
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
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
|
+
]);
|
|
13573
14586
|
} finally {
|
|
13574
|
-
|
|
14587
|
+
if (timeout) clearTimeout(timeout);
|
|
13575
14588
|
}
|
|
13576
14589
|
}
|
|
13577
14590
|
|
|
13578
|
-
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(
|
|
13579
14600
|
authorizationArg: unknown,
|
|
13580
14601
|
contextArg: Readonly<plugins.typedsocket.IVirtualStreamAuthorizationContext>,
|
|
13581
14602
|
): plugins.typedsocket.IVirtualStreamAuthorizationBinding {
|
|
13582
14603
|
if (typeof authorizationArg !== 'object' || authorizationArg === null) {
|
|
13583
|
-
throw new Error('Unknown browser
|
|
14604
|
+
throw new Error('Unknown browser stream authority.');
|
|
13584
14605
|
}
|
|
13585
|
-
const authorization = authorizationArg as Partial<
|
|
14606
|
+
const authorization = authorizationArg as Partial<IControllerBrowserStreamAuthorization>;
|
|
13586
14607
|
const view = authorization.view;
|
|
13587
14608
|
if (
|
|
13588
|
-
authorization.marker !==
|
|
14609
|
+
authorization.marker !== browserStreamAuthorizationMarker
|
|
13589
14610
|
|| !view
|
|
13590
14611
|
|| authorization.authority !== view.authority
|
|
13591
14612
|
|| authorization.transportPeer !== view.transportPeer
|
|
14613
|
+
|| typeof authorization.transportIdentity !== 'symbol'
|
|
13592
14614
|
|| contextArg.target !== view.transportPeer
|
|
13593
|
-
||
|
|
14615
|
+
|| authorization.streamGeneration !== view.streamGeneration
|
|
14616
|
+
|| contextArg.creatorDirection !== authorization.creatorDirection
|
|
13594
14617
|
|| contextArg.contentType !== authorization.contentType
|
|
13595
|
-
|| contextArg.integrity
|
|
13596
|
-
|| contextArg.integrity?.byteLength !== authorization.integrity?.byteLength
|
|
13597
|
-
|| contextArg.integrity?.digest !== authorization.integrity?.digest
|
|
14618
|
+
|| contextArg.integrity !== undefined
|
|
13598
14619
|
|| view.closing
|
|
13599
|
-
|| view.activationState === 'inactive'
|
|
13600
14620
|
|| this.browserViews.get(view.id) !== view
|
|
13601
14621
|
|| this.browserViewSlots.get(view.peerId)?.view !== view
|
|
13602
|
-
|| !this.
|
|
13603
|
-
|
|
13604
|
-
|
|
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;
|
|
13605
14629
|
return {
|
|
13606
|
-
revalidate: (revalidationArg) => this.
|
|
14630
|
+
revalidate: (revalidationArg) => this.revalidateBrowserStream(
|
|
13607
14631
|
bound,
|
|
13608
14632
|
revalidationArg,
|
|
13609
14633
|
),
|
|
13610
14634
|
};
|
|
13611
14635
|
}
|
|
13612
14636
|
|
|
13613
|
-
private async
|
|
13614
|
-
authorizationArg:
|
|
14637
|
+
private async revalidateBrowserStream(
|
|
14638
|
+
authorizationArg: IControllerBrowserStreamAuthorization,
|
|
13615
14639
|
contextArg: Readonly<plugins.typedsocket.IVirtualStreamRevalidationContext>,
|
|
13616
14640
|
): Promise<boolean> {
|
|
13617
14641
|
const view = authorizationArg.view;
|
|
@@ -13621,32 +14645,93 @@ export class OpenCodeController {
|
|
|
13621
14645
|
|| contextArg.connection.peer !== authorizationArg.transportPeer
|
|
13622
14646
|
|| view.transportPeer !== authorizationArg.transportPeer
|
|
13623
14647
|
|| view.authority !== authorizationArg.authority
|
|
13624
|
-
|| view.
|
|
13625
|
-
||
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
14648
|
+
|| view.streamGeneration !== authorizationArg.streamGeneration
|
|
14649
|
+
|| !this.isBrowserViewInMemoryAuthorityCurrent(
|
|
14650
|
+
view,
|
|
14651
|
+
authorizationArg.streamGeneration,
|
|
14652
|
+
authorizationArg.transportIdentity,
|
|
14653
|
+
)
|
|
13630
14654
|
|| view.transportPeer.context.headers.get('origin') !== this.requireRuntimeConfig().publicOrigin
|
|
13631
|
-
|| !this.isBrowserRuntimeAuthorityCurrent(view)
|
|
13632
14655
|
) return false;
|
|
13633
14656
|
try {
|
|
13634
|
-
|
|
13635
|
-
view
|
|
13636
|
-
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
&& current.pendingAttachment === undefined
|
|
13642
|
-
&& current.attachmentAuthorityId === view.resource.attachmentAuthorityId
|
|
13643
|
-
&& current.attachmentRevision === view.resource.attachmentRevision
|
|
13644
|
-
&& this.isBrowserRuntimeAuthorityCurrent(view);
|
|
14657
|
+
await this.assertBrowserViewAuthority(
|
|
14658
|
+
view,
|
|
14659
|
+
authorizationArg.streamGeneration,
|
|
14660
|
+
authorizationArg.transportIdentity,
|
|
14661
|
+
contextArg.abortSignal,
|
|
14662
|
+
);
|
|
14663
|
+
return true;
|
|
13645
14664
|
} catch {
|
|
13646
14665
|
return false;
|
|
13647
14666
|
}
|
|
13648
14667
|
}
|
|
13649
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
|
+
|
|
13650
14735
|
private isBrowserRuntimeAuthorityCurrent(viewArg: IControllerBrowserView): boolean {
|
|
13651
14736
|
try {
|
|
13652
14737
|
return viewArg.handle.lease.isAuthorityCurrent(viewArg.authority);
|
|
@@ -13655,11 +14740,14 @@ export class OpenCodeController {
|
|
|
13655
14740
|
}
|
|
13656
14741
|
}
|
|
13657
14742
|
|
|
13658
|
-
private
|
|
14743
|
+
private nextControllerBrowserViewState(
|
|
14744
|
+
viewArg: IControllerBrowserView,
|
|
13659
14745
|
stateArg: plugins.browserRuntime.IBrowserRuntimeState,
|
|
13660
|
-
):
|
|
14746
|
+
): IControllerBrowserViewState {
|
|
13661
14747
|
const { lastError, ...state } = stateArg;
|
|
14748
|
+
viewArg.stateRevision += 1;
|
|
13662
14749
|
return {
|
|
14750
|
+
revision: viewArg.stateRevision,
|
|
13663
14751
|
...state,
|
|
13664
14752
|
...(lastError === undefined ? {} : {
|
|
13665
14753
|
lastError: { ...lastError, message: lastError.code },
|