Skip to content

Commit d48a1de

Browse files
committed
feat(client+core): input/outputs
1 parent 9808ac8 commit d48a1de

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2314
-184
lines changed

client/connections/ConnectionToCore.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default abstract class ConnectionToCore extends TypedEventEmitter<{
5252
constructor(options?: IConnectionToCoreOptions) {
5353
super();
5454
this.options = options ?? { isPersistent: true };
55-
this.commandQueue = new CoreCommandQueue(null, this);
55+
this.commandQueue = new CoreCommandQueue(null, this, null);
5656
this.coreSessions = new CoreSessions(
5757
this.options.maxConcurrency,
5858
this.options.agentTimeoutMillis,
@@ -85,13 +85,15 @@ export default abstract class ConnectionToCore extends TypedEventEmitter<{
8585
if (!this.connectPromise) {
8686
this.connectPromise = new Resolvable();
8787
try {
88+
const startTime = new Date();
8889
const connectError = await this.createConnection();
8990
if (connectError) throw connectError;
9091
if (this.isDisconnecting) throw new DisconnectedFromCoreError(this.resolvedHost);
9192
// can be resolved if canceled by a disconnect
9293
if (this.connectPromise.isResolved) return;
9394

9495
const connectResult = await this.internalSendRequestAndWait({
96+
startDate: startTime,
9597
command: 'Core.connect',
9698
args: [this.connectOptions],
9799
});
@@ -122,11 +124,13 @@ export default abstract class ConnectionToCore extends TypedEventEmitter<{
122124

123125
public async disconnect(fatalError?: Error): Promise<void> {
124126
// user triggered disconnect sends a disconnect to Core
127+
const startTime = new Date();
125128
await this.internalDisconnect(fatalError, async () => {
126129
try {
127130
await this.internalSendRequestAndWait(
128131
{
129132
command: 'Core.disconnect',
133+
startDate: startTime,
130134
args: [fatalError],
131135
},
132136
2e3,
@@ -144,7 +148,7 @@ export default abstract class ConnectionToCore extends TypedEventEmitter<{
144148
/////// PIPE FUNCTIONS /////////////////////////////////////////////////////////////////////////////////////////////
145149

146150
public async sendRequest(
147-
payload: Omit<ICoreRequestPayload, 'messageId'>,
151+
payload: Omit<ICoreRequestPayload, 'messageId' | 'sendDate'>,
148152
): Promise<ICoreResponsePayload> {
149153
const result = await this.connect();
150154
if (result) throw result;
@@ -243,7 +247,7 @@ export default abstract class ConnectionToCore extends TypedEventEmitter<{
243247
}
244248

245249
protected async internalSendRequestAndWait(
246-
payload: Omit<ICoreRequestPayload, 'messageId'>,
250+
payload: Omit<ICoreRequestPayload, 'messageId' | 'sendDate'>,
247251
timeoutMs?: number,
248252
): Promise<ICoreResponsePayload> {
249253
const { promise, id, resolve } = this.createPendingResult();
@@ -257,6 +261,7 @@ export default abstract class ConnectionToCore extends TypedEventEmitter<{
257261
try {
258262
await this.internalSendRequest({
259263
messageId: id,
264+
sendDate: new Date(),
260265
...payload,
261266
});
262267
} catch (error) {
@@ -300,7 +305,7 @@ export default abstract class ConnectionToCore extends TypedEventEmitter<{
300305
}
301306
this.rejectPendingRequest(pending, responseError);
302307
} else {
303-
pending.resolve({ data: message.data, commandId: message.commandId });
308+
pending.resolve({ data: message.data });
304309
}
305310
}
306311

client/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
Tab,
1717
XPathResult,
1818
LocationStatus,
19+
Observable,
1920
LocationTrigger,
2021
} = cjsImport;
2122

@@ -33,6 +34,7 @@ export {
3334
Node,
3435
FrameEnvironment,
3536
Tab,
37+
Observable,
3638
XPathResult,
3739
LocationStatus,
3840
LocationTrigger,

client/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ import type Tab from './lib/Tab';
1515
import RemoteConnectionToCore from './connections/RemoteConnectionToCore';
1616
import ConnectionToCore from './connections/ConnectionToCore';
1717
import ConnectionFactory from './connections/ConnectionFactory';
18+
import { Observable } from './lib/ObjectObserver';
19+
import { readCommandLineArgs } from './lib/Input';
1820

19-
export default new Agent();
21+
const input = readCommandLineArgs();
22+
23+
export default new Agent({ input });
2024

2125
export {
26+
Observable,
2227
Handler,
2328
Agent,
2429
RemoteConnectionToCore,

client/interfaces/IAgentCreateOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export default interface IAgentCreateOptions
77
name?: string;
88
showReplay?: boolean;
99
connectionToCore?: IConnectionToCoreOptions | ConnectionToCore;
10+
input?: { command?: string } & any;
1011
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default interface ICommandCounter {
2+
nextCommandId: number;
3+
lastCommandId: number;
4+
}

client/lib/Agent.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import FrameEnvironment, { getCoreFrameEnvironment } from './FrameEnvironment';
5252
import getClientExtenderPluginClass from './getClientExtenderPluginClass';
5353
import FrozenTab from './FrozenTab';
5454
import FileChooser from './FileChooser';
55+
import Output, { createObservableOutput } from './Output';
5556

5657
export const DefaultOptions = {
5758
defaultBlockedResourceTypes: [BlockedResourceType.None],
@@ -73,6 +74,7 @@ const propertyKeys: (keyof Agent)[] = [
7374
'sessionId',
7475
'meta',
7576
'tabs',
77+
'output',
7678
'frameEnvironments',
7779
'mainFrameEnvironment',
7880
'coreHost',
@@ -86,6 +88,10 @@ const propertyKeys: (keyof Agent)[] = [
8688
export default class Agent extends AwaitedEventTarget<{ close: void }> implements IAgent {
8789
protected static options: IAgentDefaults = { ...DefaultOptions };
8890

91+
public readonly input: { command?: string } & any;
92+
93+
#output: Output;
94+
8995
constructor(options: IAgentCreateOptions = {}) {
9096
super(() => {
9197
return {
@@ -97,6 +103,7 @@ export default class Agent extends AwaitedEventTarget<{ close: void }> implement
97103
options.blockedResourceTypes =
98104
options.blockedResourceTypes || Agent.options.defaultBlockedResourceTypes;
99105
options.userProfile = options.userProfile || Agent.options.defaultUserProfile;
106+
this.input = options.input;
100107

101108
const sessionName = scriptInstance.generateSessionName(options.name);
102109
delete options.name;
@@ -115,6 +122,24 @@ export default class Agent extends AwaitedEventTarget<{ close: void }> implement
115122
});
116123
}
117124

125+
public get output(): any | any[] {
126+
if (!this.#output) {
127+
const coreSession = getState(this)
128+
.connection.getCoreSessionOrReject()
129+
.catch(() => null);
130+
this.#output = createObservableOutput(coreSession);
131+
}
132+
return this.#output;
133+
}
134+
135+
public set output(value: any | any[]) {
136+
const output = this.output;
137+
for (const key of Object.keys(output)) {
138+
delete output[key];
139+
}
140+
Object.assign(this.output, value);
141+
}
142+
118143
public get activeTab(): Tab {
119144
return getState(this).connection.activeTab;
120145
}
@@ -462,7 +487,8 @@ class SessionConnection {
462487
}
463488
this.hasConnected = true;
464489
const { plugins } = getState(this.agent);
465-
const { showReplay, connectionToCore, ...options } = getState(this.agent).options;
490+
const { showReplay, connectionToCore, ...options } = getState(this.agent)
491+
.options as IAgentCreateOptions;
466492

467493
const connection = ConnectionFactory.createConnection(
468494
connectionToCore ?? { isPersistent: false },

client/lib/CoreCommandQueue.ts

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,42 @@
11
import ISessionMeta from '@secret-agent/interfaces/ISessionMeta';
22
import { CanceledPromiseError } from '@secret-agent/commons/interfaces/IPendingWaitEvent';
33
import Queue from '@secret-agent/commons/Queue';
4+
import ICoreRequestPayload from '@secret-agent/interfaces/ICoreRequestPayload';
45
import ConnectionToCore from '../connections/ConnectionToCore';
56
import { convertJsPathArgs } from './SetupAwaitedHandler';
7+
import ICommandCounter from '../interfaces/ICommandCounter';
68

79
export default class CoreCommandQueue {
8-
public lastCommandId = 0;
10+
public get lastCommandId(): number {
11+
return this.commandCounter?.lastCommandId;
12+
}
13+
14+
public get nextCommandId(): number {
15+
return this.commandCounter?.nextCommandId;
16+
}
917

1018
private readonly internalState: {
1119
queue: Queue;
12-
batchSendCommands: { [command: string]: any[][] };
20+
commandsToRecord: ICoreRequestPayload['recordCommands'];
1321
};
1422

23+
private readonly commandCounter?: ICommandCounter;
1524
private readonly sessionMarker: string = '';
25+
private readonly meta: ISessionMeta;
26+
private readonly connection: ConnectionToCore;
27+
private flushOnTimeout: NodeJS.Timeout;
1628

1729
private get internalQueue(): Queue {
1830
return this.internalState.queue;
1931
}
2032

2133
constructor(
22-
private readonly meta: (ISessionMeta & { sessionName: string }) | null,
23-
private readonly connection: ConnectionToCore,
34+
meta: (ISessionMeta & { sessionName: string }) | null,
35+
connection: ConnectionToCore,
36+
commandCounter: ICommandCounter,
2437
internalState?: CoreCommandQueue['internalState'],
2538
) {
39+
this.connection = connection;
2640
if (meta) {
2741
const markers = [
2842
''.padEnd(50, '-'),
@@ -31,38 +45,47 @@ export default class CoreCommandQueue {
3145
''.padEnd(50, '-'),
3246
].join('\n');
3347
this.sessionMarker = `\n\n${markers}`;
48+
this.meta = { sessionId: meta.sessionId, tabId: meta.tabId, frameId: meta.frameId };
3449
}
50+
this.commandCounter = commandCounter;
3551

36-
if (internalState) {
37-
this.internalState = internalState;
38-
} else {
39-
this.internalState = {
40-
queue: new Queue('CORE COMMANDS', 1),
41-
batchSendCommands: {},
42-
};
43-
}
52+
this.internalState = internalState ?? {
53+
queue: new Queue('CORE COMMANDS', 1),
54+
commandsToRecord: [],
55+
};
4456
}
4557

46-
public queueBatchedCommand(command: string, ...args: any[]): void {
47-
this.internalState.batchSendCommands[command] ??= [];
48-
this.internalState.batchSendCommands[command].push(args);
49-
if (this.internalState.batchSendCommands[command].length > 1000) this.flush().catch(() => null);
50-
this.internalQueue.enqueue(this.flush.bind(this));
58+
public record(command: { command: string; args: any[]; commandId?: number }): void {
59+
this.internalState.commandsToRecord.push({
60+
...command,
61+
startDate: new Date(),
62+
});
63+
if (this.internalState.commandsToRecord.length > 1000) {
64+
this.flush().catch(() => null);
65+
} else if (!this.flushOnTimeout) {
66+
this.flushOnTimeout = setTimeout(() => this.flush(), 1e3).unref();
67+
}
5168
}
5269

5370
public async flush(): Promise<void> {
54-
for (const [command, args] of Object.entries(this.internalState.batchSendCommands)) {
55-
delete this.internalState.batchSendCommands[command];
56-
if (!args) continue;
57-
await this.connection.sendRequest({
58-
meta: this.meta,
59-
command,
60-
args,
61-
});
62-
}
71+
clearTimeout(this.flushOnTimeout);
72+
this.flushOnTimeout = null;
73+
if (!this.internalState.commandsToRecord.length) return;
74+
const recordCommands = [...this.internalState.commandsToRecord];
75+
this.internalState.commandsToRecord.length = 0;
76+
77+
await this.connection.sendRequest({
78+
meta: this.meta,
79+
command: 'Session.flush',
80+
startDate: new Date(),
81+
args: [],
82+
recordCommands,
83+
});
6384
}
6485

6586
public run<T>(command: string, ...args: any[]): Promise<T> {
87+
clearTimeout(this.flushOnTimeout);
88+
this.flushOnTimeout = null;
6689
if (this.connection.isDisconnecting) {
6790
return Promise.resolve(null);
6891
}
@@ -71,17 +94,24 @@ export default class CoreCommandQueue {
7194
convertJsPathArgs(arg);
7295
}
7396
}
97+
const startTime = new Date();
98+
const commandId = this.nextCommandId;
7499
return this.internalQueue
75100
.run<T>(async () => {
101+
const recordCommands = [...this.internalState.commandsToRecord];
102+
this.internalState.commandsToRecord.length = 0;
103+
76104
const response = await this.connection.sendRequest({
77105
meta: this.meta,
78106
command,
79107
args,
108+
startDate: startTime,
109+
commandId,
110+
recordCommands,
80111
});
81112

82113
let data: T = null;
83114
if (response) {
84-
this.lastCommandId = response.commandId;
85115
data = response.data;
86116
}
87117
return data;
@@ -101,6 +131,6 @@ export default class CoreCommandQueue {
101131
}
102132

103133
public createSharedQueue(meta: ISessionMeta & { sessionName: string }): CoreCommandQueue {
104-
return new CoreCommandQueue(meta, this.connection, this.internalState);
134+
return new CoreCommandQueue(meta, this.connection, this.commandCounter, this.internalState);
105135
}
106136
}

client/lib/CoreEventHeap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export default class CoreEventHeap {
4444

4545
const subscriptionPromise = this.connection.sendRequest({
4646
meta: this.meta,
47+
startDate: new Date(),
4748
command: 'Session.addEventListener',
4849
args: [jsPath, type, options],
4950
});
@@ -82,6 +83,7 @@ export default class CoreEventHeap {
8283
this.connection
8384
.sendRequest({
8485
meta: this.meta,
86+
startDate: new Date(),
8587
command: 'Session.removeEventListener',
8688
args: [listenerId],
8789
})

client/lib/CoreFrameEnvironment.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,12 @@ export default class CoreFrameEnvironment {
4444
return await this.commandQueue.run('FrameEnvironment.execJsPath', jsPath);
4545
}
4646

47-
public recordDetachedJsPath(index: number, startTime: Date, endTime: Date): void {
48-
this.commandQueue.queueBatchedCommand(
49-
'FrameEnvironment.recordDetachedJsPaths',
50-
index,
51-
startTime.getTime(),
52-
endTime.getTime(),
53-
);
47+
public recordDetachedJsPath(index: number, startDate: Date, endDate: Date): void {
48+
this.commandQueue.record({
49+
commandId: this.commandQueue.nextCommandId,
50+
command: 'FrameEnvironment.recordDetachedJsPath',
51+
args: [index, startDate.getTime(), endDate.getTime()],
52+
});
5453
}
5554

5655
public async getJsValue<T>(expression: string): Promise<T> {

0 commit comments

Comments
 (0)