donobu 5.52.1 → 5.52.3
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/dist/cli/donobu-cli.js +58 -0
- package/dist/envVars.d.ts +2 -0
- package/dist/envVars.js +9 -0
- package/dist/esm/cli/donobu-cli.js +58 -0
- package/dist/esm/envVars.d.ts +2 -0
- package/dist/esm/envVars.js +9 -0
- package/dist/esm/lib/test/testExtension.js +5 -2
- package/dist/esm/lib/test/utils/donobuTestStack.js +1 -1
- package/dist/esm/main.d.ts +2 -0
- package/dist/esm/main.js +1 -1
- package/dist/esm/managers/AdminApiController.d.ts +1 -1
- package/dist/esm/managers/DonobuStack.d.ts +2 -0
- package/dist/esm/managers/DonobuStack.js +1 -1
- package/dist/esm/managers/PageInspector.js +43 -15
- package/dist/esm/persistence/files/FileUploadCache.d.ts +16 -0
- package/dist/esm/persistence/files/FileUploadCache.js +30 -0
- package/dist/esm/persistence/files/FileUploadWorker.d.ts +28 -4
- package/dist/esm/persistence/files/FileUploadWorker.js +31 -7
- package/dist/esm/persistence/files/fileUploadWorkerRegistry.d.ts +9 -0
- package/dist/esm/persistence/files/fileUploadWorkerRegistry.js +16 -0
- package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.d.ts +49 -13
- package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.js +106 -39
- package/dist/esm/persistence/flows/FlowsPersistenceRegistry.d.ts +17 -1
- package/dist/esm/persistence/flows/FlowsPersistenceRegistry.js +33 -10
- package/dist/lib/test/testExtension.js +5 -2
- package/dist/lib/test/utils/donobuTestStack.js +1 -1
- package/dist/main.d.ts +2 -0
- package/dist/main.js +1 -1
- package/dist/managers/AdminApiController.d.ts +1 -1
- package/dist/managers/DonobuStack.d.ts +2 -0
- package/dist/managers/DonobuStack.js +1 -1
- package/dist/managers/PageInspector.js +43 -15
- package/dist/persistence/files/FileUploadCache.d.ts +16 -0
- package/dist/persistence/files/FileUploadCache.js +30 -0
- package/dist/persistence/files/FileUploadWorker.d.ts +28 -4
- package/dist/persistence/files/FileUploadWorker.js +31 -7
- package/dist/persistence/files/fileUploadWorkerRegistry.d.ts +9 -0
- package/dist/persistence/files/fileUploadWorkerRegistry.js +16 -0
- package/dist/persistence/flows/FlowsPersistenceDonobuApi.d.ts +49 -13
- package/dist/persistence/flows/FlowsPersistenceDonobuApi.js +106 -39
- package/dist/persistence/flows/FlowsPersistenceRegistry.d.ts +17 -1
- package/dist/persistence/flows/FlowsPersistenceRegistry.js +33 -10
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerFileUploadWorker = registerFileUploadWorker;
|
|
4
4
|
exports.unregisterFileUploadWorker = unregisterFileUploadWorker;
|
|
5
5
|
exports.getFileUploadAggregateStatus = getFileUploadAggregateStatus;
|
|
6
|
+
exports.getOwnedActiveUploadCount = getOwnedActiveUploadCount;
|
|
6
7
|
exports.shutdownFileUploadWorkers = shutdownFileUploadWorkers;
|
|
7
8
|
const Logger_1 = require("../../utils/Logger");
|
|
8
9
|
const workers = new Set();
|
|
@@ -38,6 +39,21 @@ async function getFileUploadAggregateStatus() {
|
|
|
38
39
|
}
|
|
39
40
|
return { totalPending, totalInFlight, totalFailed, perPlatform };
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Total uploads still owed by every registered worker — pending files plus
|
|
44
|
+
* each worker's own in-flight claims, excluding claims orphaned by other or
|
|
45
|
+
* crashed processes (which this process can neither upload nor release). This
|
|
46
|
+
* is what a graceful shutdown actually waits on, so use it (not the global
|
|
47
|
+
* {@link getFileUploadAggregateStatus}, which counts foreign claims too) when
|
|
48
|
+
* deciding whether a drain is needed and reporting how much is left.
|
|
49
|
+
*/
|
|
50
|
+
async function getOwnedActiveUploadCount() {
|
|
51
|
+
let total = 0;
|
|
52
|
+
for (const worker of workers) {
|
|
53
|
+
total += await worker.countOwnedActive();
|
|
54
|
+
}
|
|
55
|
+
return total;
|
|
56
|
+
}
|
|
41
57
|
/**
|
|
42
58
|
* Drains and stops every registered worker. Used during graceful shutdown.
|
|
43
59
|
* Returns a summary across all workers so the caller can log how many
|
|
@@ -4,6 +4,7 @@ import type { FlowMetadata, FlowsQuery } from '../../models/FlowMetadata';
|
|
|
4
4
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
5
5
|
import type { ToolCall } from '../../models/ToolCall';
|
|
6
6
|
import type { VideoSegment } from '../../models/VideoSegment';
|
|
7
|
+
import { FileUploadCache } from '../files/FileUploadCache';
|
|
7
8
|
import type { FlowsPersistence } from './FlowsPersistence';
|
|
8
9
|
/**
|
|
9
10
|
* A {@link FlowsPersistence} implementation that persists flow data via the
|
|
@@ -24,8 +25,20 @@ export declare class FlowsPersistenceDonobuApi implements FlowsPersistence {
|
|
|
24
25
|
private readonly baseUrl;
|
|
25
26
|
private readonly apiKey;
|
|
26
27
|
private readonly fileCache;
|
|
27
|
-
private readonly
|
|
28
|
-
|
|
28
|
+
private readonly notifyUpload;
|
|
29
|
+
/**
|
|
30
|
+
* Prefer {@link createDonobuFlowsPersistence} over calling this directly — it
|
|
31
|
+
* builds the cache and, when this process owns draining, the background
|
|
32
|
+
* uploader, and wires `notifyUpload` for you.
|
|
33
|
+
*
|
|
34
|
+
* @param fileCache shared local upload cache (bytes + `.pending` markers).
|
|
35
|
+
* @param notifyUpload invoked after each cache write to wake a co-located
|
|
36
|
+
* uploader. A no-op in producer-only mode (Playwright workers under
|
|
37
|
+
* `donobu test`), where a different process owns draining the shared cache;
|
|
38
|
+
* cross-process wake-ups are impossible anyway, so the parent's uploader
|
|
39
|
+
* polls instead.
|
|
40
|
+
*/
|
|
41
|
+
constructor(baseUrl: string, apiKey: string, fileCache: FileUploadCache, notifyUpload?: () => void);
|
|
29
42
|
private request;
|
|
30
43
|
private jsonRequest;
|
|
31
44
|
setFlowMetadata(flowMetadata: FlowMetadata): Promise<void>;
|
|
@@ -50,20 +63,43 @@ export declare class FlowsPersistenceDonobuApi implements FlowsPersistence {
|
|
|
50
63
|
*/
|
|
51
64
|
getFlowFile(flowId: string, fileId: string): Promise<Buffer | null>;
|
|
52
65
|
/**
|
|
53
|
-
* Writes bytes to the local file cache and returns immediately. The
|
|
54
|
-
*
|
|
55
|
-
* {@link FileUploadWorker}
|
|
56
|
-
*
|
|
66
|
+
* Writes bytes to the local file cache and returns immediately. The actual
|
|
67
|
+
* HTTP upload to the Donobu API runs asynchronously in a
|
|
68
|
+
* {@link FileUploadWorker} (owned by this process or, under `donobu test`, by
|
|
69
|
+
* the CLI parent) with retry + backoff. Same-machine reads during the upload
|
|
70
|
+
* window hit the local cache (no 404).
|
|
57
71
|
*/
|
|
58
72
|
setFlowFile(flowId: string, fileId: string, fileBytes: Buffer): Promise<void>;
|
|
59
|
-
/**
|
|
60
|
-
* Performs the actual HTTP PUT against the Donobu API. Called by the
|
|
61
|
-
* {@link FileUploadWorker} when draining the upload queue. Not part of
|
|
62
|
-
* the public {@link FlowsPersistence} surface — callers go through
|
|
63
|
-
* {@link setFlowFile}.
|
|
64
|
-
*/
|
|
65
|
-
private uploadFlowFileViaHttp;
|
|
66
73
|
setBrowserState(flowId: string, browserState: BrowserStorageState): Promise<void>;
|
|
67
74
|
getBrowserState(flowId: string): Promise<BrowserStorageState | null>;
|
|
68
75
|
}
|
|
76
|
+
export interface CreateDonobuFlowsPersistenceOptions {
|
|
77
|
+
readonly baseUrl: string;
|
|
78
|
+
readonly apiKey: string;
|
|
79
|
+
readonly baseWorkingDirectory?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Whether THIS process owns draining uploads to the cloud. True for a
|
|
82
|
+
* standalone process (Studio, or a raw `playwright test` worker with no CLI
|
|
83
|
+
* parent): start the background uploader here. False for a Playwright worker
|
|
84
|
+
* under `donobu test`: producer-only — write bytes to the shared cache and
|
|
85
|
+
* let the CLI parent drain them.
|
|
86
|
+
*/
|
|
87
|
+
readonly ownsUploadDraining: boolean;
|
|
88
|
+
/** Idle re-scan interval for the uploader, used by the cross-process parent
|
|
89
|
+
* daemon whose producers live in other processes (no in-process notify). */
|
|
90
|
+
readonly pollIntervalMs?: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Composition root for the Donobu persistence client. Builds the shared upload
|
|
94
|
+
* cache and, when this process owns draining, the background uploader; returns
|
|
95
|
+
* a {@link FlowsPersistenceDonobuApi} wired to notify that uploader on writes.
|
|
96
|
+
*/
|
|
97
|
+
export declare function createDonobuFlowsPersistence(options: CreateDonobuFlowsPersistenceOptions): FlowsPersistenceDonobuApi;
|
|
98
|
+
/**
|
|
99
|
+
* Starts ONLY the Donobu cloud uploader (no read/write client) for a process
|
|
100
|
+
* that owns draining but never reads or writes flows itself — the long-lived
|
|
101
|
+
* `donobu` CLI parent. The worker registers globally; drain it later via
|
|
102
|
+
* `shutdownFileUploadWorkers()`.
|
|
103
|
+
*/
|
|
104
|
+
export declare function startDonobuParentUploader(baseUrl: string, apiKey: string, pollIntervalMs: number, baseWorkingDirectory?: string): void;
|
|
69
105
|
//# sourceMappingURL=FlowsPersistenceDonobuApi.d.ts.map
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.FlowsPersistenceDonobuApi = void 0;
|
|
7
|
+
exports.createDonobuFlowsPersistence = createDonobuFlowsPersistence;
|
|
8
|
+
exports.startDonobuParentUploader = startDonobuParentUploader;
|
|
7
9
|
const path_1 = __importDefault(require("path"));
|
|
8
10
|
const FlowNotFoundException_1 = require("../../exceptions/FlowNotFoundException");
|
|
9
11
|
const BrowserStorageState_1 = require("../../models/BrowserStorageState");
|
|
@@ -29,29 +31,27 @@ const VIDEO_FILE_ID = 'video.webm';
|
|
|
29
31
|
* remain synchronous — they're small JSON, no bandwidth concern.
|
|
30
32
|
*/
|
|
31
33
|
class FlowsPersistenceDonobuApi {
|
|
32
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Prefer {@link createDonobuFlowsPersistence} over calling this directly — it
|
|
36
|
+
* builds the cache and, when this process owns draining, the background
|
|
37
|
+
* uploader, and wires `notifyUpload` for you.
|
|
38
|
+
*
|
|
39
|
+
* @param fileCache shared local upload cache (bytes + `.pending` markers).
|
|
40
|
+
* @param notifyUpload invoked after each cache write to wake a co-located
|
|
41
|
+
* uploader. A no-op in producer-only mode (Playwright workers under
|
|
42
|
+
* `donobu test`), where a different process owns draining the shared cache;
|
|
43
|
+
* cross-process wake-ups are impossible anyway, so the parent's uploader
|
|
44
|
+
* polls instead.
|
|
45
|
+
*/
|
|
46
|
+
constructor(baseUrl, apiKey, fileCache, notifyUpload = () => { }) {
|
|
33
47
|
this.baseUrl = baseUrl;
|
|
34
48
|
this.apiKey = apiKey;
|
|
35
|
-
this.fileCache =
|
|
36
|
-
this.
|
|
37
|
-
cache: this.fileCache,
|
|
38
|
-
platformLabel: PLATFORM_LABEL,
|
|
39
|
-
upload: (flowId, fileId, bytes) => this.uploadFlowFileViaHttp(flowId, fileId, bytes),
|
|
40
|
-
});
|
|
41
|
-
(0, fileUploadWorkerRegistry_1.registerFileUploadWorker)(this.fileWorker);
|
|
42
|
-
this.fileWorker.start();
|
|
49
|
+
this.fileCache = fileCache;
|
|
50
|
+
this.notifyUpload = notifyUpload;
|
|
43
51
|
}
|
|
44
52
|
// -- helpers --------------------------------------------------------
|
|
45
53
|
async request(path, init) {
|
|
46
|
-
|
|
47
|
-
const response = await fetch(url, {
|
|
48
|
-
...init,
|
|
49
|
-
headers: {
|
|
50
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
51
|
-
...init.headers,
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
return response;
|
|
54
|
+
return donobuApiFetch(this.baseUrl, this.apiKey, path, init);
|
|
55
55
|
}
|
|
56
56
|
async jsonRequest(path, method, body) {
|
|
57
57
|
return this.request(path, {
|
|
@@ -264,30 +264,15 @@ class FlowsPersistenceDonobuApi {
|
|
|
264
264
|
return buffer;
|
|
265
265
|
}
|
|
266
266
|
/**
|
|
267
|
-
* Writes bytes to the local file cache and returns immediately. The
|
|
268
|
-
*
|
|
269
|
-
* {@link FileUploadWorker}
|
|
270
|
-
*
|
|
267
|
+
* Writes bytes to the local file cache and returns immediately. The actual
|
|
268
|
+
* HTTP upload to the Donobu API runs asynchronously in a
|
|
269
|
+
* {@link FileUploadWorker} (owned by this process or, under `donobu test`, by
|
|
270
|
+
* the CLI parent) with retry + backoff. Same-machine reads during the upload
|
|
271
|
+
* window hit the local cache (no 404).
|
|
271
272
|
*/
|
|
272
273
|
async setFlowFile(flowId, fileId, fileBytes) {
|
|
273
274
|
await this.fileCache.writePending(flowId, fileId, fileBytes);
|
|
274
|
-
this.
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Performs the actual HTTP PUT against the Donobu API. Called by the
|
|
278
|
-
* {@link FileUploadWorker} when draining the upload queue. Not part of
|
|
279
|
-
* the public {@link FlowsPersistence} surface — callers go through
|
|
280
|
-
* {@link setFlowFile}.
|
|
281
|
-
*/
|
|
282
|
-
async uploadFlowFileViaHttp(flowId, fileId, fileBytes) {
|
|
283
|
-
const response = await this.request(`/v1/flows/${encodeURIComponent(flowId)}/files/${encodeURIComponent(fileId)}`, {
|
|
284
|
-
method: 'PUT',
|
|
285
|
-
headers: { 'Content-Type': 'application/octet-stream' },
|
|
286
|
-
body: new Uint8Array(fileBytes),
|
|
287
|
-
});
|
|
288
|
-
if (!response.ok) {
|
|
289
|
-
throw new Error(`Failed to set flow file: ${response.status} ${response.statusText}`);
|
|
290
|
-
}
|
|
275
|
+
this.notifyUpload();
|
|
291
276
|
}
|
|
292
277
|
// -- Browser state -------------------------------------------------
|
|
293
278
|
async setBrowserState(flowId, browserState) {
|
|
@@ -312,4 +297,86 @@ class FlowsPersistenceDonobuApi {
|
|
|
312
297
|
}
|
|
313
298
|
}
|
|
314
299
|
exports.FlowsPersistenceDonobuApi = FlowsPersistenceDonobuApi;
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Donobu API transport + upload subsystem (composition root)
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// These free functions own all construction of the Donobu cloud upload
|
|
304
|
+
// subsystem (cache + background uploader) and of the persistence client
|
|
305
|
+
// itself. Keeping them out of the class means the persistence is a plain
|
|
306
|
+
// read/write client over an injected cache + notify hook, with no knowledge of
|
|
307
|
+
// ownership policy, env, or worker lifecycle.
|
|
308
|
+
/** Authenticated fetch against the Donobu API. Shared by the persistence
|
|
309
|
+
* client (reads/writes) and the standalone upload transport below. */
|
|
310
|
+
async function donobuApiFetch(baseUrl, apiKey, path, init) {
|
|
311
|
+
return fetch(`${baseUrl}${path}`, {
|
|
312
|
+
...init,
|
|
313
|
+
headers: {
|
|
314
|
+
Authorization: `Bearer ${apiKey}`,
|
|
315
|
+
...init.headers,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* PUTs file bytes to the Donobu API. This is the upload transport handed to the
|
|
321
|
+
* {@link FileUploadWorker}; it's standalone (takes creds, not a persistence
|
|
322
|
+
* instance) precisely so a worker can be built without a
|
|
323
|
+
* {@link FlowsPersistenceDonobuApi} — which is what lets the CLI parent own an
|
|
324
|
+
* uploader without constructing a read/write client it never uses.
|
|
325
|
+
*/
|
|
326
|
+
async function uploadDonobuFlowFile(baseUrl, apiKey, flowId, fileId, fileBytes) {
|
|
327
|
+
const response = await donobuApiFetch(baseUrl, apiKey, `/v1/flows/${encodeURIComponent(flowId)}/files/${encodeURIComponent(fileId)}`, {
|
|
328
|
+
method: 'PUT',
|
|
329
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
330
|
+
body: new Uint8Array(fileBytes),
|
|
331
|
+
});
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
throw new Error(`Failed to set flow file: ${response.status} ${response.statusText}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** The local upload cache for the Donobu platform, rooted under the shared
|
|
337
|
+
* working directory so every process (CLI parent + Playwright workers) agrees
|
|
338
|
+
* on one location. */
|
|
339
|
+
function donobuUploadCache(baseWorkingDirectory = MiscUtils_1.MiscUtils.baseWorkingDirectory()) {
|
|
340
|
+
return new FileUploadCache_1.FileUploadCache(path_1.default.join(baseWorkingDirectory, 'uploads', PLATFORM_LABEL));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Builds, registers, and starts the background uploader that drains `cache` to
|
|
344
|
+
* the Donobu API. Whatever process is responsible for pushing artifacts to the
|
|
345
|
+
* cloud calls this exactly once; drain + stop it later via
|
|
346
|
+
* `shutdownFileUploadWorkers()`.
|
|
347
|
+
*/
|
|
348
|
+
function startDonobuCloudUploader(cache, baseUrl, apiKey, pollIntervalMs) {
|
|
349
|
+
const worker = new FileUploadWorker_1.FileUploadWorker({
|
|
350
|
+
cache,
|
|
351
|
+
platformLabel: PLATFORM_LABEL,
|
|
352
|
+
upload: (flowId, fileId, bytes) => uploadDonobuFlowFile(baseUrl, apiKey, flowId, fileId, bytes),
|
|
353
|
+
pollIntervalMs,
|
|
354
|
+
});
|
|
355
|
+
(0, fileUploadWorkerRegistry_1.registerFileUploadWorker)(worker);
|
|
356
|
+
worker.start();
|
|
357
|
+
return worker;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Composition root for the Donobu persistence client. Builds the shared upload
|
|
361
|
+
* cache and, when this process owns draining, the background uploader; returns
|
|
362
|
+
* a {@link FlowsPersistenceDonobuApi} wired to notify that uploader on writes.
|
|
363
|
+
*/
|
|
364
|
+
function createDonobuFlowsPersistence(options) {
|
|
365
|
+
const cache = donobuUploadCache(options.baseWorkingDirectory);
|
|
366
|
+
let notifyUpload = () => { };
|
|
367
|
+
if (options.ownsUploadDraining) {
|
|
368
|
+
const worker = startDonobuCloudUploader(cache, options.baseUrl, options.apiKey, options.pollIntervalMs);
|
|
369
|
+
notifyUpload = () => worker.notify();
|
|
370
|
+
}
|
|
371
|
+
return new FlowsPersistenceDonobuApi(options.baseUrl, options.apiKey, cache, notifyUpload);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Starts ONLY the Donobu cloud uploader (no read/write client) for a process
|
|
375
|
+
* that owns draining but never reads or writes flows itself — the long-lived
|
|
376
|
+
* `donobu` CLI parent. The worker registers globally; drain it later via
|
|
377
|
+
* `shutdownFileUploadWorkers()`.
|
|
378
|
+
*/
|
|
379
|
+
function startDonobuParentUploader(baseUrl, apiKey, pollIntervalMs, baseWorkingDirectory) {
|
|
380
|
+
startDonobuCloudUploader(donobuUploadCache(baseWorkingDirectory), baseUrl, apiKey, pollIntervalMs);
|
|
381
|
+
}
|
|
315
382
|
//# sourceMappingURL=FlowsPersistenceDonobuApi.js.map
|
|
@@ -2,6 +2,22 @@ import type { EnvPick } from 'env-struct';
|
|
|
2
2
|
import type { env } from '../../envVars';
|
|
3
3
|
import { PersistencePluginRegistry } from '../PersistencePlugin';
|
|
4
4
|
import type { FlowsPersistence } from './FlowsPersistence';
|
|
5
|
+
/** Credentials for the DONOBU cloud layer, resolved from the environment. */
|
|
6
|
+
export interface DonobuCloudConfig {
|
|
7
|
+
readonly baseUrl: string;
|
|
8
|
+
readonly apiKey: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the DONOBU cloud persistence config, or null if it isn't active for
|
|
12
|
+
* this environment (missing credentials, or `DONOBU` not in
|
|
13
|
+
* `PERSISTENCE_PRIORITY`). Single source of truth for "is DONOBU cloud on?",
|
|
14
|
+
* shared by {@link FlowsPersistenceRegistryImpl.fromEnvironment} (which builds
|
|
15
|
+
* the read/write client) and the `donobu` CLI (which starts the parent-owned
|
|
16
|
+
* uploader). The credential precedence — primary `DONOBU_API_KEY`, then the
|
|
17
|
+
* persistence-only `DONOBU_PERSISTENCE_API_KEY` — matches the inference
|
|
18
|
+
* fallback chain in DonobuFlowsManager.createGptClient.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveDonobuCloudConfig(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'DONOBU_PERSISTENCE_API_KEY' | 'PERSISTENCE_PRIORITY'>): DonobuCloudConfig | null;
|
|
5
21
|
/**
|
|
6
22
|
* A persistence layer paired with the `PERSISTENCE_PRIORITY` key it was
|
|
7
23
|
* created from. The key is shared across the flows/tests/suites registries:
|
|
@@ -46,7 +62,7 @@ export declare class FlowsPersistenceRegistryImpl implements FlowsPersistenceReg
|
|
|
46
62
|
* Creates an instance by reading environment variables and eagerly constructing
|
|
47
63
|
* all applicable persistence layers.
|
|
48
64
|
*/
|
|
49
|
-
static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'DONOBU_PERSISTENCE_API_KEY' | 'PERSISTENCE_PRIORITY'>, persistencePlugins?: PersistencePluginRegistry): Promise<FlowsPersistenceRegistryImpl>;
|
|
65
|
+
static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'DONOBU_PERSISTENCE_API_KEY' | 'DONOBU_UPLOADS_OWNED_BY_PARENT' | 'PERSISTENCE_PRIORITY'>, persistencePlugins?: PersistencePluginRegistry): Promise<FlowsPersistenceRegistryImpl>;
|
|
50
66
|
/**
|
|
51
67
|
* Returns the primary persistence layer.
|
|
52
68
|
*/
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FlowsPersistenceRegistryImpl = void 0;
|
|
4
|
+
exports.resolveDonobuCloudConfig = resolveDonobuCloudConfig;
|
|
4
5
|
const DonobuSqliteDb_1 = require("../DonobuSqliteDb");
|
|
5
6
|
const PersistencePlugin_1 = require("../PersistencePlugin");
|
|
6
7
|
const FlowsPersistenceDonobuApi_1 = require("./FlowsPersistenceDonobuApi");
|
|
7
8
|
const FlowsPersistenceSqlite_1 = require("./FlowsPersistenceSqlite");
|
|
8
9
|
const FlowsPersistenceVolatile_1 = require("./FlowsPersistenceVolatile");
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the DONOBU cloud persistence config, or null if it isn't active for
|
|
12
|
+
* this environment (missing credentials, or `DONOBU` not in
|
|
13
|
+
* `PERSISTENCE_PRIORITY`). Single source of truth for "is DONOBU cloud on?",
|
|
14
|
+
* shared by {@link FlowsPersistenceRegistryImpl.fromEnvironment} (which builds
|
|
15
|
+
* the read/write client) and the `donobu` CLI (which starts the parent-owned
|
|
16
|
+
* uploader). The credential precedence — primary `DONOBU_API_KEY`, then the
|
|
17
|
+
* persistence-only `DONOBU_PERSISTENCE_API_KEY` — matches the inference
|
|
18
|
+
* fallback chain in DonobuFlowsManager.createGptClient.
|
|
19
|
+
*/
|
|
20
|
+
function resolveDonobuCloudConfig(environ) {
|
|
21
|
+
const baseUrl = environ.data.DONOBU_API_BASE_URL;
|
|
22
|
+
const apiKey = environ.data.DONOBU_API_KEY ?? environ.data.DONOBU_PERSISTENCE_API_KEY;
|
|
23
|
+
if (!baseUrl ||
|
|
24
|
+
!apiKey ||
|
|
25
|
+
!environ.data.PERSISTENCE_PRIORITY.includes('DONOBU')) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return { baseUrl, apiKey };
|
|
29
|
+
}
|
|
9
30
|
/**
|
|
10
31
|
* A factory class for creating FlowsPersistence instances. Persistence layers are constructed
|
|
11
32
|
* eagerly at creation time and reused across all subsequent calls.
|
|
@@ -25,22 +46,24 @@ class FlowsPersistenceRegistryImpl {
|
|
|
25
46
|
* all applicable persistence layers.
|
|
26
47
|
*/
|
|
27
48
|
static async fromEnvironment(environ, persistencePlugins = new PersistencePlugin_1.PersistencePluginRegistry()) {
|
|
28
|
-
const
|
|
29
|
-
// Persistence credential resolution: prefer the primary DONOBU_API_KEY
|
|
30
|
-
// (which also drives the inference fallback chain in
|
|
31
|
-
// DonobuFlowsManager.createGptClient), then fall back to the
|
|
32
|
-
// persistence-only DONOBU_PERSISTENCE_API_KEY for hosts that route
|
|
33
|
-
// inference through an explicit gpt-config and don't want their
|
|
34
|
-
// persistence credential to short-circuit the agent lookup.
|
|
35
|
-
const donobuApiKey = environ.data.DONOBU_API_KEY ?? environ.data.DONOBU_PERSISTENCE_API_KEY;
|
|
49
|
+
const donobuCloud = resolveDonobuCloudConfig(environ);
|
|
36
50
|
const layers = [];
|
|
37
51
|
for (const key of environ.data.PERSISTENCE_PRIORITY) {
|
|
38
52
|
switch (key) {
|
|
39
53
|
case 'DONOBU':
|
|
40
|
-
if (
|
|
54
|
+
if (donobuCloud) {
|
|
41
55
|
layers.push({
|
|
42
56
|
key,
|
|
43
|
-
|
|
57
|
+
// Read the internal coordination flag here, at the env boundary,
|
|
58
|
+
// rather than inside the persistence client. When `donobu test`
|
|
59
|
+
// set it on this (worker) process, the worker runs producer-only
|
|
60
|
+
// and the CLI parent owns draining; otherwise this process owns
|
|
61
|
+
// its own uploader (Studio, or raw `playwright test`).
|
|
62
|
+
persistence: (0, FlowsPersistenceDonobuApi_1.createDonobuFlowsPersistence)({
|
|
63
|
+
baseUrl: donobuCloud.baseUrl,
|
|
64
|
+
apiKey: donobuCloud.apiKey,
|
|
65
|
+
ownsUploadDraining: !environ.data.DONOBU_UPLOADS_OWNED_BY_PARENT,
|
|
66
|
+
}),
|
|
44
67
|
});
|
|
45
68
|
}
|
|
46
69
|
break;
|
|
@@ -333,8 +333,11 @@ exports.test = test_1.test.extend({
|
|
|
333
333
|
}
|
|
334
334
|
let initialActive;
|
|
335
335
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
// Owned count only: pending + this process's own in-flight claims.
|
|
337
|
+
// Claims orphaned by other/crashed processes are excluded — this
|
|
338
|
+
// process can neither upload nor release them, so waiting on them
|
|
339
|
+
// would wedge worker teardown until the grace timeout force-kills it.
|
|
340
|
+
initialActive = await (0, fileUploadWorkerRegistry_1.getOwnedActiveUploadCount)();
|
|
338
341
|
}
|
|
339
342
|
catch (err) {
|
|
340
343
|
Logger_1.appLogger.warn(`donobuFileUploadDrainGuard: failed to read upload status; ` +
|
|
@@ -20,7 +20,7 @@ function getEnvSnapshot() {
|
|
|
20
20
|
}
|
|
21
21
|
async function getOrCreateDonobuStack() {
|
|
22
22
|
if (!donobuStack) {
|
|
23
|
-
donobuStack = await (0, DonobuStack_1.setupDonobuStack)('LOCAL', ControlPanel_1.NoOpControlPanelFactory, new EnvPersistenceVolatile_1.EnvPersistenceVolatile(getEnvSnapshot()), envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'DONOBU_PERSISTENCE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY'));
|
|
23
|
+
donobuStack = await (0, DonobuStack_1.setupDonobuStack)('LOCAL', ControlPanel_1.NoOpControlPanelFactory, new EnvPersistenceVolatile_1.EnvPersistenceVolatile(getEnvSnapshot()), envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'DONOBU_PERSISTENCE_API_KEY', 'DONOBU_UPLOADS_OWNED_BY_PARENT', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY'));
|
|
24
24
|
return donobuStack;
|
|
25
25
|
}
|
|
26
26
|
else {
|
package/dist/main.d.ts
CHANGED
|
@@ -105,6 +105,7 @@ export declare function startDonobuServer({ port, controlPanelHost, environ, }?:
|
|
|
105
105
|
AWS_SECRET_ACCESS_KEY: z.ZodOptional<z.ZodString>;
|
|
106
106
|
DONOBU_API_KEY: z.ZodOptional<z.ZodString>;
|
|
107
107
|
DONOBU_PERSISTENCE_API_KEY: z.ZodOptional<z.ZodString>;
|
|
108
|
+
DONOBU_UPLOADS_OWNED_BY_PARENT: z.ZodOptional<z.ZodCodec<z.ZodString, z.ZodBoolean>>;
|
|
108
109
|
}, {
|
|
109
110
|
BASE64_GPT_CONFIG?: string | undefined;
|
|
110
111
|
BROWSERBASE_API_KEY?: string | undefined;
|
|
@@ -125,6 +126,7 @@ export declare function startDonobuServer({ port, controlPanelHost, environ, }?:
|
|
|
125
126
|
AWS_SECRET_ACCESS_KEY?: string | undefined;
|
|
126
127
|
DONOBU_API_KEY?: string | undefined;
|
|
127
128
|
DONOBU_PERSISTENCE_API_KEY?: string | undefined;
|
|
129
|
+
DONOBU_UPLOADS_OWNED_BY_PARENT?: boolean | undefined;
|
|
128
130
|
}> | undefined;
|
|
129
131
|
}): Promise<void>;
|
|
130
132
|
//# sourceMappingURL=main.d.ts.map
|
package/dist/main.js
CHANGED
|
@@ -111,7 +111,7 @@ const DEFAULT_PORT = 31000;
|
|
|
111
111
|
* Starts a Donobu API server at the given port. The server assumes that the
|
|
112
112
|
* Playwright browsers have been installed.
|
|
113
113
|
*/
|
|
114
|
-
async function startDonobuServer({ port = DEFAULT_PORT, controlPanelHost = ControlPanel_1.NoOpControlPanelFactory, environ = envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_DEPLOYMENT_ENVIRONMENT', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'DONOBU_PERSISTENCE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY'), } = {}) {
|
|
114
|
+
async function startDonobuServer({ port = DEFAULT_PORT, controlPanelHost = ControlPanel_1.NoOpControlPanelFactory, environ = envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_DEPLOYMENT_ENVIRONMENT', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'DONOBU_PERSISTENCE_API_KEY', 'DONOBU_UPLOADS_OWNED_BY_PARENT', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY'), } = {}) {
|
|
115
115
|
try {
|
|
116
116
|
const adminController = await AdminApiController_1.AdminApiController.create(environ.data.DONOBU_DEPLOYMENT_ENVIRONMENT ?? 'LOCAL', controlPanelHost, environ);
|
|
117
117
|
await adminController.start(port);
|
|
@@ -59,7 +59,7 @@ export declare class AdminApiController {
|
|
|
59
59
|
* - no checking for flow ownership is performed, as all flows are considered owned by the
|
|
60
60
|
* local environment.
|
|
61
61
|
*/
|
|
62
|
-
static create(donobuDeploymentEnvironment: DonobuDeploymentEnvironment, controlPanelFactory: ControlPanelFactory, environ: EnvPick<typeof env, 'ANTHROPIC_API_KEY' | 'ANTHROPIC_MODEL_NAME' | 'AWS_ACCESS_KEY_ID' | 'AWS_BEDROCK_MODEL_NAME' | 'AWS_SECRET_ACCESS_KEY' | 'BASE64_GPT_CONFIG' | 'BROWSERBASE_API_KEY' | 'BROWSERBASE_PROJECT_ID' | 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'DONOBU_PERSISTENCE_API_KEY' | 'GOOGLE_GENERATIVE_AI_API_KEY' | 'GOOGLE_GENERATIVE_AI_MODEL_NAME' | 'OLLAMA_API_URL' | 'OLLAMA_MODEL_NAME' | 'OPENAI_API_KEY' | 'OPENAI_API_MODEL_NAME' | 'PERSISTENCE_PRIORITY'>): Promise<AdminApiController>;
|
|
62
|
+
static create(donobuDeploymentEnvironment: DonobuDeploymentEnvironment, controlPanelFactory: ControlPanelFactory, environ: EnvPick<typeof env, 'ANTHROPIC_API_KEY' | 'ANTHROPIC_MODEL_NAME' | 'AWS_ACCESS_KEY_ID' | 'AWS_BEDROCK_MODEL_NAME' | 'AWS_SECRET_ACCESS_KEY' | 'BASE64_GPT_CONFIG' | 'BROWSERBASE_API_KEY' | 'BROWSERBASE_PROJECT_ID' | 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'DONOBU_PERSISTENCE_API_KEY' | 'DONOBU_UPLOADS_OWNED_BY_PARENT' | 'GOOGLE_GENERATIVE_AI_API_KEY' | 'GOOGLE_GENERATIVE_AI_MODEL_NAME' | 'OLLAMA_API_URL' | 'OLLAMA_MODEL_NAME' | 'OPENAI_API_KEY' | 'OPENAI_API_MODEL_NAME' | 'PERSISTENCE_PRIORITY'>): Promise<AdminApiController>;
|
|
63
63
|
private constructor();
|
|
64
64
|
/**
|
|
65
65
|
* Binds the API/web-asset server to `port` and resolves once the socket is
|
|
@@ -61,6 +61,7 @@ export declare function setupDonobuStack(donobuDeploymentEnvironment: DonobuDepl
|
|
|
61
61
|
AWS_SECRET_ACCESS_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
|
|
62
62
|
DONOBU_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
|
|
63
63
|
DONOBU_PERSISTENCE_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
|
|
64
|
+
DONOBU_UPLOADS_OWNED_BY_PARENT: import("zod/v4").ZodOptional<import("zod/v4").ZodCodec<import("zod/v4").ZodString, import("zod/v4").ZodBoolean>>;
|
|
64
65
|
}, {
|
|
65
66
|
BASE64_GPT_CONFIG?: string | undefined;
|
|
66
67
|
BROWSERBASE_API_KEY?: string | undefined;
|
|
@@ -80,5 +81,6 @@ export declare function setupDonobuStack(donobuDeploymentEnvironment: DonobuDepl
|
|
|
80
81
|
AWS_SECRET_ACCESS_KEY?: string | undefined;
|
|
81
82
|
DONOBU_API_KEY?: string | undefined;
|
|
82
83
|
DONOBU_PERSISTENCE_API_KEY?: string | undefined;
|
|
84
|
+
DONOBU_UPLOADS_OWNED_BY_PARENT?: boolean | undefined;
|
|
83
85
|
}>): Promise<DonobuStack>;
|
|
84
86
|
//# sourceMappingURL=DonobuStack.d.ts.map
|
|
@@ -33,7 +33,7 @@ const ToolRegistry_1 = require("./ToolRegistry");
|
|
|
33
33
|
* then having this snapshot is relevant so that tests can use normal
|
|
34
34
|
* environment variables.
|
|
35
35
|
*/
|
|
36
|
-
async function setupDonobuStack(donobuDeploymentEnvironment, controlPanelFactory, envPersistenceVolatile, environ = envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'DONOBU_PERSISTENCE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY')) {
|
|
36
|
+
async function setupDonobuStack(donobuDeploymentEnvironment, controlPanelFactory, envPersistenceVolatile, environ = envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'DONOBU_PERSISTENCE_API_KEY', 'DONOBU_UPLOADS_OWNED_BY_PARENT', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY')) {
|
|
37
37
|
const loadedPlugins = await loadDefaultPlugins();
|
|
38
38
|
const resolvedToolRegistry = await (0, ToolRegistry_1.createDefaultToolRegistry)(loadedPlugins.tools);
|
|
39
39
|
const persistencePlugins = new PersistencePlugin_1.PersistencePluginRegistry(loadedPlugins.persistencePlugins);
|
|
@@ -933,24 +933,22 @@ class PageInspector {
|
|
|
933
933
|
if (document.scrollingElement) {
|
|
934
934
|
maybeAddScrollable(document.scrollingElement);
|
|
935
935
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
// Skip if this element already carries a value (e.g. assigned via <label>)
|
|
946
|
-
return;
|
|
936
|
+
/**
|
|
937
|
+
* Run the visibility / enabled / top-most checks on a single element and,
|
|
938
|
+
* if they pass, assign it the next interactable number. Returns `true` if
|
|
939
|
+
* the element (or, via the <label htmlFor> mapping, its associated control)
|
|
940
|
+
* was attributed.
|
|
941
|
+
*/
|
|
942
|
+
function tryAttributeElement(element) {
|
|
943
|
+
if (element.hasAttribute(interactableAttribute)) {
|
|
944
|
+
return false;
|
|
947
945
|
}
|
|
948
946
|
const rect = element.getBoundingClientRect();
|
|
949
947
|
const style = window.getComputedStyle(element);
|
|
950
948
|
const visible = isElementVisible(rect, style) && isElementMoreThanHalfInViewport(rect);
|
|
951
949
|
const enabled = isElementEnabled(element, style);
|
|
952
950
|
if (!visible || !enabled) {
|
|
953
|
-
return;
|
|
951
|
+
return false;
|
|
954
952
|
}
|
|
955
953
|
// Check a few probe points to make sure the element is top-most
|
|
956
954
|
for (const pt of getPointsToCheck(rect)) {
|
|
@@ -959,9 +957,9 @@ class PageInspector {
|
|
|
959
957
|
if (elToCheck === element) {
|
|
960
958
|
element.setAttribute(interactableAttribute, offset.toString());
|
|
961
959
|
offset++;
|
|
962
|
-
return; // this element done
|
|
960
|
+
return true; // this element done
|
|
963
961
|
}
|
|
964
|
-
// Handle <label> -> control mapping
|
|
962
|
+
// Handle <label> -> control mapping (explicit `for`/`htmlFor`)
|
|
965
963
|
if (elToCheck.tagName.toLowerCase() === 'label' &&
|
|
966
964
|
elToCheck.htmlFor) {
|
|
967
965
|
const forId = elToCheck.htmlFor;
|
|
@@ -972,11 +970,41 @@ class PageInspector {
|
|
|
972
970
|
control.setAttribute(interactableAttribute, offset.toString());
|
|
973
971
|
offset++;
|
|
974
972
|
}
|
|
975
|
-
return;
|
|
973
|
+
return true;
|
|
976
974
|
}
|
|
977
975
|
elToCheck = elToCheck.parentElement;
|
|
978
976
|
}
|
|
979
977
|
}
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
// 2) Iterate and assign numbers
|
|
981
|
+
uniqueElements.forEach((element) => {
|
|
982
|
+
if (element === document.scrollingElement) {
|
|
983
|
+
// Special-case: always keep the root scrolling element
|
|
984
|
+
element.setAttribute(interactableAttribute, offset.toString());
|
|
985
|
+
offset++;
|
|
986
|
+
return; // skip the usual checks
|
|
987
|
+
}
|
|
988
|
+
else if (element.hasAttribute(interactableAttribute)) {
|
|
989
|
+
// Skip if this element already carries a value (e.g. assigned via <label>)
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (tryAttributeElement(element)) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
// Fallback: the element is a visually-hidden native control (e.g. a 0x0,
|
|
996
|
+
// opacity-0, pointer-events:none <input>) wrapped in a styled <label>.
|
|
997
|
+
// This is the standard pattern for Ant Design Segmented/Radio/Checkbox/
|
|
998
|
+
// Switch and many other component libraries: the native input is hidden
|
|
999
|
+
// and the surrounding <label> is the real clickable surface. Since the
|
|
1000
|
+
// hidden control fails the visibility/enabled checks above, attribute the
|
|
1001
|
+
// wrapping <label> instead so the toggle is still annotated.
|
|
1002
|
+
const wrappingLabel = element.closest('label');
|
|
1003
|
+
if (wrappingLabel &&
|
|
1004
|
+
wrappingLabel !== element &&
|
|
1005
|
+
!wrappingLabel.hasAttribute(interactableAttribute)) {
|
|
1006
|
+
tryAttributeElement(wrappingLabel);
|
|
1007
|
+
}
|
|
980
1008
|
});
|
|
981
1009
|
return offset;
|
|
982
1010
|
}
|
|
@@ -94,6 +94,22 @@ export declare class FileUploadCache {
|
|
|
94
94
|
* active work even though `listPending` excludes it.
|
|
95
95
|
*/
|
|
96
96
|
countActive(): Promise<number>;
|
|
97
|
+
/**
|
|
98
|
+
* Like {@link countActive}, but scoped to the work THIS worker is
|
|
99
|
+
* responsible for finishing before its process exits: files still
|
|
100
|
+
* `.pending` (which this worker will claim and upload) plus this worker's
|
|
101
|
+
* own `.uploading.<ownToken>` claims.
|
|
102
|
+
*
|
|
103
|
+
* Foreign `.uploading.<otherToken>` markers — in-flight claims owned by
|
|
104
|
+
* OTHER (possibly long-dead) processes — are deliberately EXCLUDED. A live
|
|
105
|
+
* peer is responsible for finishing its own claims, and claims orphaned by
|
|
106
|
+
* a crashed peer are recovered by {@link reclaimStaleClaims} on the next
|
|
107
|
+
* long-lived process. Counting them here is exactly what made an
|
|
108
|
+
* end-of-worker drain block for the full timeout on a crashed peer's
|
|
109
|
+
* orphaned claim (which this process can neither upload nor release),
|
|
110
|
+
* wedging Playwright worker teardown until it was force-killed.
|
|
111
|
+
*/
|
|
112
|
+
countOwnedActive(ownToken: string): Promise<number>;
|
|
97
113
|
/**
|
|
98
114
|
* Returns a breakdown of marker counts on disk. Used by the admin
|
|
99
115
|
* "uploads status" endpoint so the desktop app can show the user how
|
|
@@ -178,6 +178,36 @@ class FileUploadCache {
|
|
|
178
178
|
const counts = await this.getCounts();
|
|
179
179
|
return counts.pending + counts.inFlight;
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Like {@link countActive}, but scoped to the work THIS worker is
|
|
183
|
+
* responsible for finishing before its process exits: files still
|
|
184
|
+
* `.pending` (which this worker will claim and upload) plus this worker's
|
|
185
|
+
* own `.uploading.<ownToken>` claims.
|
|
186
|
+
*
|
|
187
|
+
* Foreign `.uploading.<otherToken>` markers — in-flight claims owned by
|
|
188
|
+
* OTHER (possibly long-dead) processes — are deliberately EXCLUDED. A live
|
|
189
|
+
* peer is responsible for finishing its own claims, and claims orphaned by
|
|
190
|
+
* a crashed peer are recovered by {@link reclaimStaleClaims} on the next
|
|
191
|
+
* long-lived process. Counting them here is exactly what made an
|
|
192
|
+
* end-of-worker drain block for the full timeout on a crashed peer's
|
|
193
|
+
* orphaned claim (which this process can neither upload nor release),
|
|
194
|
+
* wedging Playwright worker teardown until it was force-killed.
|
|
195
|
+
*/
|
|
196
|
+
async countOwnedActive(ownToken) {
|
|
197
|
+
const ownSuffix = `${UPLOADING_PREFIX}${ownToken}`;
|
|
198
|
+
let active = 0;
|
|
199
|
+
const flowDirs = await this.safeReaddir(this.platformDir);
|
|
200
|
+
for (const encFlow of flowDirs) {
|
|
201
|
+
const flowDir = path_1.default.join(this.platformDir, encFlow);
|
|
202
|
+
const names = await this.safeReaddir(flowDir);
|
|
203
|
+
for (const name of names) {
|
|
204
|
+
if (name.endsWith(PENDING_SUFFIX) || name.endsWith(ownSuffix)) {
|
|
205
|
+
active += 1;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return active;
|
|
210
|
+
}
|
|
181
211
|
/**
|
|
182
212
|
* Returns a breakdown of marker counts on disk. Used by the admin
|
|
183
213
|
* "uploads status" endpoint so the desktop app can show the user how
|