@opengeni/interaction 0.2.2
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/LICENSE +190 -0
- package/README.md +22 -0
- package/dist/controller-core.d.ts +113 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +571 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
- package/src/controller-core.ts +487 -0
- package/src/index.ts +363 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrowserActionCommand,
|
|
3
|
+
BrowserActionReceipt,
|
|
4
|
+
BrowserObservation,
|
|
5
|
+
BrowserTarget,
|
|
6
|
+
ComputerActionCommand,
|
|
7
|
+
ComputerActionReceipt,
|
|
8
|
+
ComputerObservation,
|
|
9
|
+
ComputerTarget,
|
|
10
|
+
type BrowserActionCommand as BrowserActionCommandValue,
|
|
11
|
+
type BrowserActionReceipt as BrowserActionReceiptValue,
|
|
12
|
+
type BrowserObservation as BrowserObservationValue,
|
|
13
|
+
type BrowserTarget as BrowserTargetValue,
|
|
14
|
+
type ComputerActionCommand as ComputerActionCommandValue,
|
|
15
|
+
type ComputerActionReceipt as ComputerActionReceiptValue,
|
|
16
|
+
type ComputerObservation as ComputerObservationValue,
|
|
17
|
+
type ComputerTarget as ComputerTargetValue,
|
|
18
|
+
} from "@opengeni/contracts";
|
|
19
|
+
import {
|
|
20
|
+
InteractionControllerCore,
|
|
21
|
+
InteractionControllerError,
|
|
22
|
+
InteractionDefiniteDriverError,
|
|
23
|
+
recoverInteractionReceipt,
|
|
24
|
+
type InteractionControllerErrorCode,
|
|
25
|
+
type InteractionOperationJournalRecord,
|
|
26
|
+
} from "./controller-core";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
InteractionControllerError,
|
|
30
|
+
InteractionDefiniteDriverError,
|
|
31
|
+
type InteractionControllerErrorCode,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type BrowserInteractionDriver = {
|
|
35
|
+
target(targetId: string): Promise<BrowserTargetValue | null>;
|
|
36
|
+
observe(targetId: string): Promise<BrowserObservationValue>;
|
|
37
|
+
validate?(command: BrowserActionCommandValue, target: BrowserTargetValue): Promise<void> | void;
|
|
38
|
+
dispatch(command: BrowserActionCommandValue): Promise<BrowserObservationValue>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type BrowserInteractionAuthority = {
|
|
42
|
+
authorizeDispatch(command: BrowserActionCommandValue): Promise<void> | void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type BrowserOperationJournalRecord =
|
|
46
|
+
InteractionOperationJournalRecord<BrowserActionReceiptValue>;
|
|
47
|
+
|
|
48
|
+
export type BrowserInteractionControllerOptions = {
|
|
49
|
+
browserSessionId: string;
|
|
50
|
+
controllerGeneration: string;
|
|
51
|
+
driver: BrowserInteractionDriver;
|
|
52
|
+
authority?: BrowserInteractionAuthority;
|
|
53
|
+
maxJournalEntries?: number;
|
|
54
|
+
now?: () => Date;
|
|
55
|
+
initialJournal?: readonly BrowserOperationJournalRecord[];
|
|
56
|
+
onJournalRecord?: (record: BrowserOperationJournalRecord) => Promise<void> | void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Placement-resident BrowserSession mutation authority. */
|
|
60
|
+
export class BrowserInteractionController {
|
|
61
|
+
private readonly core: InteractionControllerCore<
|
|
62
|
+
BrowserActionCommandValue,
|
|
63
|
+
BrowserTargetValue,
|
|
64
|
+
BrowserObservationValue,
|
|
65
|
+
BrowserActionReceiptValue
|
|
66
|
+
>;
|
|
67
|
+
|
|
68
|
+
constructor(options: BrowserInteractionControllerOptions) {
|
|
69
|
+
const { browserSessionId, controllerGeneration } = options;
|
|
70
|
+
this.core = new InteractionControllerCore({
|
|
71
|
+
driver: options.driver,
|
|
72
|
+
...(options.authority ? { authority: options.authority } : {}),
|
|
73
|
+
...(options.maxJournalEntries !== undefined
|
|
74
|
+
? { maxJournalEntries: options.maxJournalEntries }
|
|
75
|
+
: {}),
|
|
76
|
+
...(options.now ? { now: options.now } : {}),
|
|
77
|
+
...(options.initialJournal ? { initialJournal: options.initialJournal } : {}),
|
|
78
|
+
...(options.onJournalRecord ? { onJournalRecord: options.onJournalRecord } : {}),
|
|
79
|
+
adapter: {
|
|
80
|
+
resourceLabel: "browser",
|
|
81
|
+
parseCommand: (value) => BrowserActionCommand.parse(value),
|
|
82
|
+
parseTarget: (value) => BrowserTarget.parse(value),
|
|
83
|
+
parseObservation: (value) => BrowserObservation.parse(value),
|
|
84
|
+
parseReceipt: (value) => BrowserActionReceipt.parse(value),
|
|
85
|
+
assertCommandAuthority(command) {
|
|
86
|
+
if (command.browserSessionId !== browserSessionId) {
|
|
87
|
+
throw new InteractionControllerError(
|
|
88
|
+
"resource_not_found",
|
|
89
|
+
"browser command targets another browser session",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (command.controllerGeneration !== controllerGeneration) {
|
|
93
|
+
throw new InteractionControllerError(
|
|
94
|
+
"controller_stale",
|
|
95
|
+
"browser command targets a stale controller generation",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
assertTargetAuthority(target) {
|
|
100
|
+
if (
|
|
101
|
+
target.browserSessionId !== browserSessionId ||
|
|
102
|
+
target.controllerGeneration !== controllerGeneration
|
|
103
|
+
) {
|
|
104
|
+
throw new InteractionControllerError(
|
|
105
|
+
"controller_stale",
|
|
106
|
+
"browser target belongs to a stale controller generation",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
assertExpectedGenerations(command, target) {
|
|
111
|
+
if (command.expectedTargetGeneration !== target.targetGeneration) {
|
|
112
|
+
throw new InteractionControllerError(
|
|
113
|
+
"target_stale",
|
|
114
|
+
"browser target changed before the command could dispatch",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (command.expectedDocumentGeneration !== target.documentGeneration) {
|
|
118
|
+
throw new InteractionControllerError(
|
|
119
|
+
"document_stale",
|
|
120
|
+
"browser document changed before the command could dispatch",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
assertObservationAuthority(observation, targetId) {
|
|
125
|
+
if (
|
|
126
|
+
observation.browserSessionId !== browserSessionId ||
|
|
127
|
+
observation.target.id !== targetId ||
|
|
128
|
+
observation.target.browserSessionId !== browserSessionId ||
|
|
129
|
+
observation.target.controllerGeneration !== controllerGeneration
|
|
130
|
+
) {
|
|
131
|
+
throw new InteractionControllerError(
|
|
132
|
+
"driver_failed",
|
|
133
|
+
"browser driver returned an observation outside controller authority",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
makeReceipt(input) {
|
|
138
|
+
return BrowserActionReceipt.parse({
|
|
139
|
+
protocolVersion: 1,
|
|
140
|
+
operationId: input.command.operationId,
|
|
141
|
+
browserSessionId,
|
|
142
|
+
controllerGeneration,
|
|
143
|
+
targetId: input.command.targetId,
|
|
144
|
+
state: input.state,
|
|
145
|
+
dispatchedAt: input.dispatchedAt,
|
|
146
|
+
settledAt: input.settledAt,
|
|
147
|
+
observation: input.observation,
|
|
148
|
+
error: input.error,
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
recoverReceipt(receipt, settledAt) {
|
|
152
|
+
if (
|
|
153
|
+
receipt.browserSessionId !== browserSessionId ||
|
|
154
|
+
receipt.controllerGeneration !== controllerGeneration
|
|
155
|
+
) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`restored operation ${receipt.operationId} is outside controller authority`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return recoverInteractionReceipt(receipt, settledAt, "browser", (value) =>
|
|
161
|
+
BrowserActionReceipt.parse(value),
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
observe(targetId: string): Promise<BrowserObservationValue> {
|
|
169
|
+
return this.core.observe(targetId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
run(command: BrowserActionCommandValue): Promise<BrowserActionReceiptValue> {
|
|
173
|
+
return this.core.run(command);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
receipt(operationId: string): BrowserActionReceiptValue | null {
|
|
177
|
+
return this.core.receipt(operationId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
journalSnapshot(): BrowserOperationJournalRecord[] {
|
|
181
|
+
return this.core.journalSnapshot();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
waitForIdle(): Promise<void> {
|
|
185
|
+
return this.core.waitForIdle();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function recoverBrowserOperationJournalRecord(
|
|
190
|
+
record: BrowserOperationJournalRecord,
|
|
191
|
+
settledAt: string,
|
|
192
|
+
): BrowserOperationJournalRecord {
|
|
193
|
+
const receipt = BrowserActionReceipt.parse(record.receipt);
|
|
194
|
+
return {
|
|
195
|
+
...record,
|
|
196
|
+
receipt: recoverInteractionReceipt(receipt, settledAt, "browser", (value) =>
|
|
197
|
+
BrowserActionReceipt.parse(value),
|
|
198
|
+
),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type ComputerInteractionDriver = {
|
|
203
|
+
target(targetId: string): Promise<ComputerTargetValue | null>;
|
|
204
|
+
observe(targetId: string): Promise<ComputerObservationValue>;
|
|
205
|
+
validate?(command: ComputerActionCommandValue, target: ComputerTargetValue): Promise<void> | void;
|
|
206
|
+
dispatch(command: ComputerActionCommandValue): Promise<ComputerObservationValue>;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export type ComputerInteractionAuthority = {
|
|
210
|
+
authorizeDispatch(command: ComputerActionCommandValue): Promise<void> | void;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export type ComputerOperationJournalRecord =
|
|
214
|
+
InteractionOperationJournalRecord<ComputerActionReceiptValue>;
|
|
215
|
+
|
|
216
|
+
export type ComputerInteractionControllerOptions = {
|
|
217
|
+
computerSessionId: string;
|
|
218
|
+
controllerGeneration: string;
|
|
219
|
+
driver: ComputerInteractionDriver;
|
|
220
|
+
authority?: ComputerInteractionAuthority;
|
|
221
|
+
maxJournalEntries?: number;
|
|
222
|
+
now?: () => Date;
|
|
223
|
+
initialJournal?: readonly ComputerOperationJournalRecord[];
|
|
224
|
+
onJournalRecord?: (record: ComputerOperationJournalRecord) => Promise<void> | void;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Placement-resident ComputerSession mutation authority. Linux AT-SPI/X11,
|
|
228
|
+
* macOS AX/ScreenCaptureKit, and later UIA adapters share this exact core. */
|
|
229
|
+
export class ComputerInteractionController {
|
|
230
|
+
private readonly core: InteractionControllerCore<
|
|
231
|
+
ComputerActionCommandValue,
|
|
232
|
+
ComputerTargetValue,
|
|
233
|
+
ComputerObservationValue,
|
|
234
|
+
ComputerActionReceiptValue
|
|
235
|
+
>;
|
|
236
|
+
|
|
237
|
+
constructor(options: ComputerInteractionControllerOptions) {
|
|
238
|
+
const { computerSessionId, controllerGeneration } = options;
|
|
239
|
+
this.core = new InteractionControllerCore({
|
|
240
|
+
driver: options.driver,
|
|
241
|
+
...(options.authority ? { authority: options.authority } : {}),
|
|
242
|
+
...(options.maxJournalEntries !== undefined
|
|
243
|
+
? { maxJournalEntries: options.maxJournalEntries }
|
|
244
|
+
: {}),
|
|
245
|
+
...(options.now ? { now: options.now } : {}),
|
|
246
|
+
...(options.initialJournal ? { initialJournal: options.initialJournal } : {}),
|
|
247
|
+
...(options.onJournalRecord ? { onJournalRecord: options.onJournalRecord } : {}),
|
|
248
|
+
adapter: {
|
|
249
|
+
resourceLabel: "computer",
|
|
250
|
+
parseCommand: (value) => ComputerActionCommand.parse(value),
|
|
251
|
+
parseTarget: (value) => ComputerTarget.parse(value),
|
|
252
|
+
parseObservation: (value) => ComputerObservation.parse(value),
|
|
253
|
+
parseReceipt: (value) => ComputerActionReceipt.parse(value),
|
|
254
|
+
assertCommandAuthority(command) {
|
|
255
|
+
if (command.computerSessionId !== computerSessionId) {
|
|
256
|
+
throw new InteractionControllerError(
|
|
257
|
+
"resource_not_found",
|
|
258
|
+
"computer command targets another computer session",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (command.controllerGeneration !== controllerGeneration) {
|
|
262
|
+
throw new InteractionControllerError(
|
|
263
|
+
"controller_stale",
|
|
264
|
+
"computer command targets a stale controller generation",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
assertTargetAuthority(target) {
|
|
269
|
+
if (
|
|
270
|
+
target.computerSessionId !== computerSessionId ||
|
|
271
|
+
target.controllerGeneration !== controllerGeneration
|
|
272
|
+
) {
|
|
273
|
+
throw new InteractionControllerError(
|
|
274
|
+
"controller_stale",
|
|
275
|
+
"computer target belongs to a stale controller generation",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
assertExpectedGenerations(command, target) {
|
|
280
|
+
if (command.expectedTargetGeneration !== target.targetGeneration) {
|
|
281
|
+
throw new InteractionControllerError(
|
|
282
|
+
"target_stale",
|
|
283
|
+
"computer target changed before the command could dispatch",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
assertObservationAuthority(observation, targetId) {
|
|
288
|
+
if (
|
|
289
|
+
observation.computerSessionId !== computerSessionId ||
|
|
290
|
+
observation.target.id !== targetId ||
|
|
291
|
+
observation.target.computerSessionId !== computerSessionId ||
|
|
292
|
+
observation.target.controllerGeneration !== controllerGeneration
|
|
293
|
+
) {
|
|
294
|
+
throw new InteractionControllerError(
|
|
295
|
+
"driver_failed",
|
|
296
|
+
"computer driver returned an observation outside controller authority",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
makeReceipt(input) {
|
|
301
|
+
return ComputerActionReceipt.parse({
|
|
302
|
+
protocolVersion: 1,
|
|
303
|
+
operationId: input.command.operationId,
|
|
304
|
+
computerSessionId,
|
|
305
|
+
controllerGeneration,
|
|
306
|
+
targetId: input.command.targetId,
|
|
307
|
+
state: input.state,
|
|
308
|
+
dispatchedAt: input.dispatchedAt,
|
|
309
|
+
settledAt: input.settledAt,
|
|
310
|
+
observation: input.observation,
|
|
311
|
+
error: input.error,
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
recoverReceipt(receipt, settledAt) {
|
|
315
|
+
if (
|
|
316
|
+
receipt.computerSessionId !== computerSessionId ||
|
|
317
|
+
receipt.controllerGeneration !== controllerGeneration
|
|
318
|
+
) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`restored operation ${receipt.operationId} is outside controller authority`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return recoverInteractionReceipt(receipt, settledAt, "computer", (value) =>
|
|
324
|
+
ComputerActionReceipt.parse(value),
|
|
325
|
+
);
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
observe(targetId: string): Promise<ComputerObservationValue> {
|
|
332
|
+
return this.core.observe(targetId);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
run(command: ComputerActionCommandValue): Promise<ComputerActionReceiptValue> {
|
|
336
|
+
return this.core.run(command);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
receipt(operationId: string): ComputerActionReceiptValue | null {
|
|
340
|
+
return this.core.receipt(operationId);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
journalSnapshot(): ComputerOperationJournalRecord[] {
|
|
344
|
+
return this.core.journalSnapshot();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
waitForIdle(): Promise<void> {
|
|
348
|
+
return this.core.waitForIdle();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function recoverComputerOperationJournalRecord(
|
|
353
|
+
record: ComputerOperationJournalRecord,
|
|
354
|
+
settledAt: string,
|
|
355
|
+
): ComputerOperationJournalRecord {
|
|
356
|
+
const receipt = ComputerActionReceipt.parse(record.receipt);
|
|
357
|
+
return {
|
|
358
|
+
...record,
|
|
359
|
+
receipt: recoverInteractionReceipt(receipt, settledAt, "computer", (value) =>
|
|
360
|
+
ComputerActionReceipt.parse(value),
|
|
361
|
+
),
|
|
362
|
+
};
|
|
363
|
+
}
|