harnery 0.45.1 → 0.46.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/dist/commander.d.ts +13 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commands/admission.d.ts.map +1 -1
- package/dist/commands/admission.js +214 -27
- package/dist/commands/agents.d.ts +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +102 -25
- package/dist/commands/browse.d.ts +3 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +172 -8
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +8 -2
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +40 -0
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +4 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +33 -6
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -45
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +0 -18
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -4
- package/dist/core/agents/render/session-context.d.ts +0 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +1 -6
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -4
- package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +6 -2
- package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
- package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference-contract.js +1 -0
- package/dist/core/agents/state/parent-reference.d.ts +18 -0
- package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference.js +119 -0
- package/dist/core/agents/state/pidmap.js +2 -2
- package/dist/core/agents/state/proc-start.d.ts +20 -31
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +37 -70
- package/dist/core/agents/state/service-process.d.ts +61 -0
- package/dist/core/agents/state/service-process.d.ts.map +1 -0
- package/dist/core/agents/state/service-process.js +143 -0
- package/dist/core/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- package/dist/core/context/index.d.ts +6 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +194 -256
- package/dist/core/context/observation.d.ts +9 -9
- package/dist/core/context/observation.d.ts.map +1 -1
- package/dist/core/context/observation.js +37 -6
- package/dist/core/diagnostics/path-identity.d.ts +1 -1
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
- package/dist/core/diagnostics/path-identity.js +51 -6
- package/dist/core/events/v3/canonical.js +43 -14
- package/dist/core/events/v3/index.d.ts +1 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -1
- package/dist/core/events/v3/producers/recorder.d.ts +34 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +117 -13
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +53 -13
- package/dist/core/events/v3/runtime-build.d.ts +14 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +44 -0
- package/dist/core/events/v3/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- package/dist/core/governor/index.d.ts +2 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +2 -1
- package/dist/core/governor/service-read.d.ts +3 -10
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +10 -94
- package/dist/core/governor/service.d.ts +3 -7
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +17 -182
- package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
- package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
- package/dist/core/hooks/adapter/native-parent.js +91 -0
- package/dist/core/hooks/cli.js +79 -4
- package/dist/core/hooks/resolve/owner.d.ts +0 -11
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +2 -25
- package/dist/core/hooks/shell-admission.d.ts +84 -0
- package/dist/core/hooks/shell-admission.d.ts.map +1 -0
- package/dist/core/hooks/shell-admission.js +622 -0
- package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
- package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
- package/dist/core/hooks/shell-commit-tree.js +104 -0
- package/dist/core/inbox/contract.d.ts +12 -0
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/contract.js +16 -0
- package/dist/core/inbox/peer-messages.d.ts +41 -16
- package/dist/core/inbox/peer-messages.d.ts.map +1 -1
- package/dist/core/inbox/peer-messages.js +372 -15
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +83 -3
- package/dist/core/inbox/targets.d.ts +17 -3
- package/dist/core/inbox/targets.d.ts.map +1 -1
- package/dist/core/inbox/targets.js +77 -2
- package/dist/core/presence/git.d.ts +18 -12
- package/dist/core/presence/git.d.ts.map +1 -1
- package/dist/core/presence/git.js +32 -38
- package/dist/core/presence/index.d.ts +2 -2
- package/dist/core/presence/index.d.ts.map +1 -1
- package/dist/core/presence/index.js +8 -6
- package/dist/core/presence/mail.d.ts +133 -0
- package/dist/core/presence/mail.d.ts.map +1 -0
- package/dist/core/presence/mail.js +464 -0
- package/dist/core/resources/sampler.js +2 -2
- package/dist/core/resources/service-status.d.ts +2 -14
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +3 -23
- package/dist/core/resources/service.d.ts +3 -7
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +12 -101
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +3 -2
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +9 -2
- package/dist/core/servers/index.d.ts +0 -2
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +3 -18
- package/dist/core/storage/modes.d.ts +4 -0
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +4 -0
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +5 -4
- package/dist/core/supervisor/service.d.ts +4 -7
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +23 -122
- package/dist/core/supervisor/services.js +2 -2
- package/dist/core/supervisor/status.d.ts +3 -17
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +7 -70
- package/dist/core/workflow/engine.js +2 -1
- package/dist/core/workflow/index.d.ts +3 -4
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +30 -74
- package/dist/core/workflow/resume-lock.d.ts +17 -0
- package/dist/core/workflow/resume-lock.d.ts.map +1 -0
- package/dist/core/workflow/resume-lock.js +158 -0
- package/dist/core/workflow/run-state.d.ts +0 -21
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -174
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -1
- package/dist/core/workflow/workspaces/local-git.js +91 -210
- package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/ownership.js +3 -130
- package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +0 -4
- package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +1 -4
- package/dist/lib/admission-history.d.ts +96 -0
- package/dist/lib/admission-history.d.ts.map +1 -0
- package/dist/lib/admission-history.js +196 -0
- package/dist/lib/admission.d.ts +56 -7
- package/dist/lib/admission.d.ts.map +1 -1
- package/dist/lib/admission.js +161 -19
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +100 -12
- package/dist/lib/browser/content-checks.d.ts +3 -1
- package/dist/lib/browser/content-checks.d.ts.map +1 -1
- package/dist/lib/browser/content-checks.js +10 -2
- package/dist/lib/browser/critique.d.ts +32 -0
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +72 -3
- package/dist/lib/browser/geometry.d.ts +2 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +61 -15
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -1
- package/dist/lib/browser/page-review-contracts.d.ts +6 -1
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
- package/dist/lib/browser/page-review-contracts.js +6 -1
- package/dist/lib/browser/page-review-judge.d.ts +3 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +41 -11
- package/dist/lib/browser/page-review-pack.d.ts +3 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +5 -2
- package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +11 -4
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +73 -16
- package/dist/lib/browser/runts.d.ts +4 -2
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +84 -33
- package/dist/lib/browser/windows-chrome.d.ts +14 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +42 -0
- package/dist/lib/browser/windows-login-sync.d.ts +110 -0
- package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
- package/dist/lib/browser/windows-login-sync.js +171 -0
- package/dist/lib/cookies/client.d.ts +30 -5
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +55 -10
- package/dist/lib/cookies/index.d.ts +1 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +1 -1
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
- package/dist/lib/docs-metadata-sync.js +41 -2
- package/dist/lib/durable-job.d.ts +8 -0
- package/dist/lib/durable-job.d.ts.map +1 -1
- package/dist/lib/durable-job.js +10 -0
- package/dist/lib/http/client.d.ts +9 -8
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +44 -2
- package/dist/lib/instructions/templates.js +4 -4
- package/package.json +2 -2
- package/src/commander.ts +13 -0
- package/src/commands/admission.ts +272 -29
- package/src/commands/agents.ts +132 -27
- package/src/commands/browse.ts +208 -6
- package/src/commands/fetch.ts +1 -0
- package/src/commands/ledger-v3.ts +8 -2
- package/src/commands/presence.ts +49 -0
- package/src/commands/qa-run.ts +4 -3
- package/src/commands/review-pack.ts +41 -5
- package/src/commands/workflow.ts +8 -58
- package/src/core/agents/coord-client.ts +0 -20
- package/src/core/agents/index.ts +6 -0
- package/src/core/agents/live-authority-v3.ts +11 -4
- package/src/core/agents/render/session-context.ts +1 -7
- package/src/core/agents/session-events.ts +18 -3
- package/src/core/agents/state/heartbeat-reader.ts +3 -0
- package/src/core/agents/state/live-coordination-view.ts +13 -6
- package/src/core/agents/state/parent-reference-contract.ts +14 -0
- package/src/core/agents/state/parent-reference.ts +179 -0
- package/src/core/agents/state/pidmap.ts +2 -2
- package/src/core/agents/state/proc-start.ts +41 -69
- package/src/core/agents/state/service-process.ts +217 -0
- package/src/core/config.ts +85 -0
- package/src/core/context/index.ts +207 -272
- package/src/core/context/observation.ts +40 -9
- package/src/core/diagnostics/path-identity.ts +52 -6
- package/src/core/events/v3/canonical.ts +46 -13
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/producers/recorder.ts +151 -13
- package/src/core/events/v3/reader.ts +55 -11
- package/src/core/events/v3/runtime-build.ts +62 -0
- package/src/core/events/v3/validate.ts +42 -1
- package/src/core/governor/index.ts +1 -2
- package/src/core/governor/service-read.ts +15 -114
- package/src/core/governor/service.ts +36 -220
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +83 -4
- package/src/core/hooks/resolve/owner.ts +2 -25
- package/src/core/hooks/shell-admission.ts +634 -0
- package/src/core/hooks/shell-commit-tree.ts +102 -0
- package/src/core/inbox/contract.ts +18 -0
- package/src/core/inbox/peer-messages.ts +501 -15
- package/src/core/inbox/service.ts +108 -3
- package/src/core/inbox/targets.ts +94 -3
- package/src/core/presence/git.ts +45 -37
- package/src/core/presence/index.ts +16 -8
- package/src/core/presence/mail.ts +620 -0
- package/src/core/resources/sampler.ts +3 -2
- package/src/core/resources/service-status.ts +6 -35
- package/src/core/resources/service.ts +23 -133
- package/src/core/resources/status.ts +3 -2
- package/src/core/semantic/service.ts +8 -2
- package/src/core/servers/index.ts +3 -23
- package/src/core/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- package/src/core/supervisor/service.ts +33 -155
- package/src/core/supervisor/services.ts +2 -2
- package/src/core/supervisor/status.ts +10 -85
- package/src/core/workflow/engine.ts +1 -1
- package/src/core/workflow/index.ts +2 -13
- package/src/core/workflow/proof.ts +35 -83
- package/src/core/workflow/resume-lock.ts +179 -0
- package/src/core/workflow/run-state.ts +3 -207
- package/src/core/workflow/workspaces/leases.ts +1 -1
- package/src/core/workflow/workspaces/local-git.ts +120 -239
- package/src/core/workflow/workspaces/ownership.ts +3 -158
- package/src/core/workflow/workspaces/reclaim.ts +3 -9
- package/src/core/workflow/workspaces/recovery.ts +1 -1
- package/src/core/workflow/workspaces/state.ts +1 -5
- package/src/lib/admission-history.ts +267 -0
- package/src/lib/admission.ts +217 -19
- package/src/lib/browser/client.ts +147 -35
- package/src/lib/browser/content-checks.ts +14 -3
- package/src/lib/browser/critique.ts +113 -3
- package/src/lib/browser/geometry.ts +63 -14
- package/src/lib/browser/index.ts +5 -0
- package/src/lib/browser/page-review-contracts.ts +6 -1
- package/src/lib/browser/page-review-judge.ts +58 -13
- package/src/lib/browser/page-review-pack.ts +13 -2
- package/src/lib/browser/qa-run-contracts.ts +30 -4
- package/src/lib/browser/qa-run.ts +78 -17
- package/src/lib/browser/runts.ts +85 -37
- package/src/lib/browser/windows-chrome.ts +50 -0
- package/src/lib/browser/windows-login-sync.ts +275 -0
- package/src/lib/cookies/client.ts +62 -10
- package/src/lib/cookies/index.ts +3 -0
- package/src/lib/docs-metadata-sync.ts +44 -2
- package/src/lib/durable-job.ts +15 -0
- package/src/lib/http/client.ts +63 -1
- package/src/lib/instructions/templates.ts +4 -4
- package/dist/core/agents/state/process-handle.d.ts +0 -26
- package/dist/core/agents/state/process-handle.d.ts.map +0 -1
- package/dist/core/agents/state/process-handle.js +0 -121
- package/dist/core/workflow/recovery-audit.d.ts +0 -30
- package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
- package/dist/core/workflow/recovery-audit.js +0 -116
- package/src/core/agents/state/process-handle.ts +0 -165
- package/src/core/workflow/recovery-audit.ts +0 -181
package/src/lib/admission.ts
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
// file; process death leaves a dead-PID file for the next participant to
|
|
19
19
|
// prune.
|
|
20
20
|
//
|
|
21
|
+
// Every ticket that ends (released, timed out, or pruned after its process
|
|
22
|
+
// died) appends one record to the admission history (admission-history.ts).
|
|
23
|
+
//
|
|
21
24
|
// Toolkit tier: this module must not import src/core (layering check).
|
|
22
25
|
|
|
23
26
|
import { randomUUID } from "node:crypto";
|
|
@@ -31,8 +34,17 @@ import {
|
|
|
31
34
|
} from "node:fs";
|
|
32
35
|
import { tmpdir } from "node:os";
|
|
33
36
|
import { join } from "node:path";
|
|
37
|
+
import {
|
|
38
|
+
ADMISSION_HISTORY_SCHEMA_VERSION,
|
|
39
|
+
type AdmissionHistoryOutcome,
|
|
40
|
+
type AdmissionReleaseResult,
|
|
41
|
+
appendAdmissionHistory,
|
|
42
|
+
resolveAdmissionHistoryDir,
|
|
43
|
+
} from "./admission-history.ts";
|
|
34
44
|
import { coordEnv } from "./env.ts";
|
|
35
45
|
|
|
46
|
+
export type { AdmissionReleaseResult } from "./admission-history.ts";
|
|
47
|
+
|
|
36
48
|
export interface AdmissionConfig {
|
|
37
49
|
/** Base directory holding one subdirectory per resource. */
|
|
38
50
|
dir: string;
|
|
@@ -44,6 +56,11 @@ export interface AdmissionConfig {
|
|
|
44
56
|
pollMs?: number;
|
|
45
57
|
/** Entries older than this are pruned regardless of PID (default 6h). */
|
|
46
58
|
ttlMs?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Directory for the slot-use history. Omitted: resolved from the queue
|
|
61
|
+
* directory (see resolveAdmissionHistoryDir). null: record nothing.
|
|
62
|
+
*/
|
|
63
|
+
historyDir?: string | null;
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
export interface AdmissionEntry {
|
|
@@ -52,6 +69,12 @@ export interface AdmissionEntry {
|
|
|
52
69
|
created_at: string;
|
|
53
70
|
/** Set on holders once admitted. */
|
|
54
71
|
acquired_at?: string;
|
|
72
|
+
/** Working directory of the queueing process. */
|
|
73
|
+
cwd?: string;
|
|
74
|
+
/** Capacity the queueing process asked for. */
|
|
75
|
+
capacity?: number;
|
|
76
|
+
/** Set on holders that had to wait: the holder labels they first waited behind. */
|
|
77
|
+
waited_behind?: string[];
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
export interface AdmissionWaitInfo {
|
|
@@ -67,6 +90,8 @@ export interface AdmissionAcquireOptions {
|
|
|
67
90
|
timeoutMs?: number;
|
|
68
91
|
/** Progress callback, invoked at most once per poll while waiting. */
|
|
69
92
|
onWait?: (info: AdmissionWaitInfo) => void;
|
|
93
|
+
/** Time the caller already spent deferring for memory pressure, for the history. */
|
|
94
|
+
pressureWaitMs?: number;
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
export interface AdmissionHandle {
|
|
@@ -74,8 +99,11 @@ export interface AdmissionHandle {
|
|
|
74
99
|
entry: string;
|
|
75
100
|
/** Milliseconds spent waiting in the queue. */
|
|
76
101
|
waitedMs: number;
|
|
77
|
-
/**
|
|
78
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Give the slot back and record the slot use in the history. Safe to call
|
|
104
|
+
* more than once; only the first call records.
|
|
105
|
+
*/
|
|
106
|
+
release: (result?: AdmissionReleaseResult) => void;
|
|
79
107
|
}
|
|
80
108
|
|
|
81
109
|
export class AdmissionTimeoutError extends Error {
|
|
@@ -143,7 +171,13 @@ interface LiveEntry {
|
|
|
143
171
|
/** List live entries in lexical (= enqueue) order, pruning dead-PID, expired,
|
|
144
172
|
* and torn files as a side effect. Every participant prunes, so a crashed
|
|
145
173
|
* holder's slot frees as soon as anyone else looks. */
|
|
146
|
-
|
|
174
|
+
type PruneReason = Extract<AdmissionHistoryOutcome, "abandoned" | "expired">;
|
|
175
|
+
|
|
176
|
+
function listLive(
|
|
177
|
+
dir: string,
|
|
178
|
+
ttlMs: number,
|
|
179
|
+
onPrune?: (entry: AdmissionEntry, reason: PruneReason) => void,
|
|
180
|
+
): LiveEntry[] {
|
|
147
181
|
let names: string[];
|
|
148
182
|
try {
|
|
149
183
|
names = readdirSync(dir)
|
|
@@ -167,6 +201,15 @@ function listLive(dir: string, ttlMs: number): LiveEntry[] {
|
|
|
167
201
|
label: typeof record.label === "string" ? record.label : "",
|
|
168
202
|
created_at: record.created_at,
|
|
169
203
|
...(typeof record.acquired_at === "string" ? { acquired_at: record.acquired_at } : {}),
|
|
204
|
+
...(typeof record.cwd === "string" ? { cwd: record.cwd } : {}),
|
|
205
|
+
...(typeof record.capacity === "number" ? { capacity: record.capacity } : {}),
|
|
206
|
+
...(Array.isArray(record.waited_behind)
|
|
207
|
+
? {
|
|
208
|
+
waited_behind: record.waited_behind.filter(
|
|
209
|
+
(label): label is string => typeof label === "string",
|
|
210
|
+
),
|
|
211
|
+
}
|
|
212
|
+
: {}),
|
|
170
213
|
};
|
|
171
214
|
}
|
|
172
215
|
}
|
|
@@ -188,12 +231,16 @@ function listLive(dir: string, ttlMs: number): LiveEntry[] {
|
|
|
188
231
|
continue;
|
|
189
232
|
}
|
|
190
233
|
const age = now - Date.parse(entry.created_at);
|
|
191
|
-
|
|
234
|
+
const dead = !pidAlive(entry.pid);
|
|
235
|
+
if (dead || Number.isNaN(age) || age > ttlMs) {
|
|
236
|
+
let pruned = false;
|
|
192
237
|
try {
|
|
193
238
|
unlinkSync(path);
|
|
239
|
+
pruned = true;
|
|
194
240
|
} catch {
|
|
195
|
-
// a peer pruned it first
|
|
241
|
+
// a peer pruned it first, and records it
|
|
196
242
|
}
|
|
243
|
+
if (pruned) onPrune?.(entry, dead ? "abandoned" : "expired");
|
|
197
244
|
continue;
|
|
198
245
|
}
|
|
199
246
|
live.push({ name, entry });
|
|
@@ -201,6 +248,42 @@ function listLive(dir: string, ttlMs: number): LiveEntry[] {
|
|
|
201
248
|
return live;
|
|
202
249
|
}
|
|
203
250
|
|
|
251
|
+
function historyDirFor(config: Pick<AdmissionConfig, "dir" | "historyDir">): string | null {
|
|
252
|
+
return config.historyDir !== undefined
|
|
253
|
+
? config.historyDir
|
|
254
|
+
: resolveAdmissionHistoryDir(config.dir);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Record an entry a participant pruned because its process died or it expired. */
|
|
258
|
+
function recordPruned(
|
|
259
|
+
historyDir: string | null,
|
|
260
|
+
resource: string,
|
|
261
|
+
entry: AdmissionEntry,
|
|
262
|
+
reason: PruneReason,
|
|
263
|
+
): void {
|
|
264
|
+
if (!historyDir) return;
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
const enqueuedMs = Date.parse(entry.created_at);
|
|
267
|
+
const acquiredMs = entry.acquired_at ? Date.parse(entry.acquired_at) : Number.NaN;
|
|
268
|
+
appendAdmissionHistory(historyDir, {
|
|
269
|
+
v: ADMISSION_HISTORY_SCHEMA_VERSION,
|
|
270
|
+
resource,
|
|
271
|
+
label: entry.label,
|
|
272
|
+
pid: entry.pid,
|
|
273
|
+
...(entry.cwd ? { cwd: entry.cwd } : {}),
|
|
274
|
+
...(entry.capacity !== undefined ? { capacity: entry.capacity } : {}),
|
|
275
|
+
outcome: reason,
|
|
276
|
+
enqueued_at: entry.created_at,
|
|
277
|
+
...(entry.acquired_at ? { acquired_at: entry.acquired_at } : {}),
|
|
278
|
+
ended_at: new Date(now).toISOString(),
|
|
279
|
+
wait_ms: Number.isNaN(enqueuedMs)
|
|
280
|
+
? 0
|
|
281
|
+
: Math.max(0, (Number.isNaN(acquiredMs) ? now : acquiredMs) - enqueuedMs),
|
|
282
|
+
...(Number.isNaN(acquiredMs) ? {} : { hold_ms: Math.max(0, now - acquiredMs) }),
|
|
283
|
+
...(entry.waited_behind?.length ? { waited_behind: entry.waited_behind } : {}),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
204
287
|
function sleep(ms: number): Promise<void> {
|
|
205
288
|
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
206
289
|
}
|
|
@@ -224,11 +307,18 @@ export async function acquireAdmission(
|
|
|
224
307
|
const tickets = ticketsDir(config);
|
|
225
308
|
const held = heldDir(config);
|
|
226
309
|
|
|
310
|
+
const historyDir = historyDirFor(config);
|
|
311
|
+
const onPrune = (entry: AdmissionEntry, reason: PruneReason): void =>
|
|
312
|
+
recordPruned(historyDir, config.resource, entry, reason);
|
|
227
313
|
const myEntry: AdmissionEntry = {
|
|
228
314
|
pid: process.pid,
|
|
229
315
|
label: options.label,
|
|
230
316
|
created_at: new Date().toISOString(),
|
|
317
|
+
cwd: process.cwd(),
|
|
318
|
+
capacity: config.capacity,
|
|
231
319
|
};
|
|
320
|
+
let waitedBehind: string[] | undefined;
|
|
321
|
+
const pressureWait = options.pressureWaitMs ? { pressure_wait_ms: options.pressureWaitMs } : {};
|
|
232
322
|
const myName = `${String(Date.now()).padStart(13, "0")}-${process.pid}-${randomUUID().slice(0, 8)}.json`;
|
|
233
323
|
writeEntry(tickets, myName, myEntry);
|
|
234
324
|
|
|
@@ -236,8 +326,8 @@ export async function acquireAdmission(
|
|
|
236
326
|
const deadline = startedWaiting + timeoutMs;
|
|
237
327
|
try {
|
|
238
328
|
while (true) {
|
|
239
|
-
const liveHeld = listLive(held, ttlMs);
|
|
240
|
-
const liveTickets = listLive(tickets, ttlMs);
|
|
329
|
+
const liveHeld = listLive(held, ttlMs, onPrune);
|
|
330
|
+
const liveTickets = listLive(tickets, ttlMs, onPrune);
|
|
241
331
|
const myIndex = liveTickets.findIndex((ticket) => ticket.name === myName);
|
|
242
332
|
if (myIndex === -1) {
|
|
243
333
|
// Pruned by a peer (should not happen while we are alive) or lost to
|
|
@@ -253,7 +343,7 @@ export async function acquireAdmission(
|
|
|
253
343
|
// between two waiters' listings. Lexical order (= enqueue order)
|
|
254
344
|
// decides who stays; the loser rejoins the queue with its original
|
|
255
345
|
// priority because the filename keeps its enqueue timestamp.
|
|
256
|
-
const heldNow = listLive(held, ttlMs);
|
|
346
|
+
const heldNow = listLive(held, ttlMs, onPrune);
|
|
257
347
|
if (heldNow.length > config.capacity) {
|
|
258
348
|
const keepers = new Set(
|
|
259
349
|
heldNow.slice(0, config.capacity).map((holderEntry) => holderEntry.name),
|
|
@@ -264,12 +354,14 @@ export async function acquireAdmission(
|
|
|
264
354
|
continue;
|
|
265
355
|
}
|
|
266
356
|
}
|
|
267
|
-
|
|
357
|
+
const acquiredAt = new Date();
|
|
358
|
+
writeEntry(held, myName, { ...myEntry, acquired_at: acquiredAt.toISOString() });
|
|
359
|
+
const waitedMs = acquiredAt.getTime() - startedWaiting;
|
|
268
360
|
let released = false;
|
|
269
361
|
return {
|
|
270
362
|
entry: myName,
|
|
271
|
-
waitedMs
|
|
272
|
-
release: () => {
|
|
363
|
+
waitedMs,
|
|
364
|
+
release: (result?: AdmissionReleaseResult) => {
|
|
273
365
|
if (released) return;
|
|
274
366
|
released = true;
|
|
275
367
|
try {
|
|
@@ -277,14 +369,55 @@ export async function acquireAdmission(
|
|
|
277
369
|
} catch {
|
|
278
370
|
// already pruned
|
|
279
371
|
}
|
|
372
|
+
const endedAt = new Date();
|
|
373
|
+
appendAdmissionHistory(historyDir, {
|
|
374
|
+
v: ADMISSION_HISTORY_SCHEMA_VERSION,
|
|
375
|
+
resource: config.resource,
|
|
376
|
+
label: myEntry.label,
|
|
377
|
+
pid: myEntry.pid,
|
|
378
|
+
...(myEntry.cwd ? { cwd: myEntry.cwd } : {}),
|
|
379
|
+
capacity: config.capacity,
|
|
380
|
+
outcome: "released",
|
|
381
|
+
enqueued_at: myEntry.created_at,
|
|
382
|
+
acquired_at: acquiredAt.toISOString(),
|
|
383
|
+
ended_at: endedAt.toISOString(),
|
|
384
|
+
wait_ms: waitedMs,
|
|
385
|
+
hold_ms: endedAt.getTime() - acquiredAt.getTime(),
|
|
386
|
+
...(waitedBehind?.length ? { waited_behind: waitedBehind } : {}),
|
|
387
|
+
...pressureWait,
|
|
388
|
+
...(result?.exitCode !== undefined ? { exit_code: result.exitCode } : {}),
|
|
389
|
+
...(result?.signal !== undefined ? { signal: result.signal } : {}),
|
|
390
|
+
});
|
|
280
391
|
},
|
|
281
392
|
};
|
|
282
393
|
}
|
|
394
|
+
if (waitedBehind === undefined) {
|
|
395
|
+
// Persist who this ticket waits behind, so the record survives even
|
|
396
|
+
// if this process dies in the queue and a peer prunes the ticket.
|
|
397
|
+
waitedBehind = liveHeld.map((holderEntry) => holderEntry.entry.label);
|
|
398
|
+
myEntry.waited_behind = waitedBehind;
|
|
399
|
+
writeEntry(tickets, myName, myEntry);
|
|
400
|
+
}
|
|
283
401
|
options.onWait?.({
|
|
284
402
|
position: myIndex + 1,
|
|
285
403
|
holders: liveHeld.map((holderEntry) => holderEntry.entry),
|
|
286
404
|
});
|
|
287
405
|
if (Date.now() >= deadline) {
|
|
406
|
+
const endedAt = new Date();
|
|
407
|
+
appendAdmissionHistory(historyDir, {
|
|
408
|
+
v: ADMISSION_HISTORY_SCHEMA_VERSION,
|
|
409
|
+
resource: config.resource,
|
|
410
|
+
label: myEntry.label,
|
|
411
|
+
pid: myEntry.pid,
|
|
412
|
+
...(myEntry.cwd ? { cwd: myEntry.cwd } : {}),
|
|
413
|
+
capacity: config.capacity,
|
|
414
|
+
outcome: "timeout",
|
|
415
|
+
enqueued_at: myEntry.created_at,
|
|
416
|
+
ended_at: endedAt.toISOString(),
|
|
417
|
+
wait_ms: endedAt.getTime() - startedWaiting,
|
|
418
|
+
...(waitedBehind?.length ? { waited_behind: waitedBehind } : {}),
|
|
419
|
+
...pressureWait,
|
|
420
|
+
});
|
|
288
421
|
throw new AdmissionTimeoutError(
|
|
289
422
|
config.resource,
|
|
290
423
|
timeoutMs,
|
|
@@ -313,25 +446,90 @@ export interface AdmissionStatus {
|
|
|
313
446
|
|
|
314
447
|
/** Snapshot the queue for one resource, pruning dead entries on the way. */
|
|
315
448
|
export function admissionStatus(
|
|
316
|
-
config: Pick<AdmissionConfig, "dir" | "resource" | "ttlMs">,
|
|
449
|
+
config: Pick<AdmissionConfig, "dir" | "resource" | "ttlMs" | "historyDir">,
|
|
317
450
|
): AdmissionStatus {
|
|
318
451
|
const ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
|
|
319
452
|
const full: AdmissionConfig = { ...config, capacity: 1 };
|
|
453
|
+
const historyDir = historyDirFor(config);
|
|
454
|
+
const onPrune = (entry: AdmissionEntry, reason: PruneReason): void =>
|
|
455
|
+
recordPruned(historyDir, config.resource, entry, reason);
|
|
320
456
|
return {
|
|
321
457
|
resource: config.resource,
|
|
322
|
-
holders: listLive(heldDir(full), ttlMs).map((holderEntry) => holderEntry.entry),
|
|
323
|
-
waiters: listLive(ticketsDir(full), ttlMs).map((ticket) => ticket.entry),
|
|
458
|
+
holders: listLive(heldDir(full), ttlMs, onPrune).map((holderEntry) => holderEntry.entry),
|
|
459
|
+
waiters: listLive(ticketsDir(full), ttlMs, onPrune).map((ticket) => ticket.entry),
|
|
324
460
|
};
|
|
325
461
|
}
|
|
326
462
|
|
|
327
463
|
/**
|
|
328
|
-
* Base directory for admission queue state.
|
|
329
|
-
* is correct
|
|
330
|
-
* HARNERY_ADMISSION_DIR. Host CLIs and harn commands must share
|
|
331
|
-
* so they join the same machine-wide queues.
|
|
464
|
+
* Base directory for admission queue state. The temp directory clears on
|
|
465
|
+
* reboot, which is correct: stale queue state must not outlive the boot.
|
|
466
|
+
* Override with HARNERY_ADMISSION_DIR. Host CLIs and harn commands must share
|
|
467
|
+
* this helper so they join the same machine-wide queues.
|
|
468
|
+
*
|
|
469
|
+
* On POSIX the default is the literal /tmp, not os.tmpdir(): agent sandboxes
|
|
470
|
+
* often set a private TMPDIR per session, and a queue under it would admit
|
|
471
|
+
* every session on its own.
|
|
332
472
|
*/
|
|
333
473
|
export function admissionBaseDir(): string {
|
|
334
|
-
return
|
|
474
|
+
return (
|
|
475
|
+
coordEnv("ADMISSION_DIR") ??
|
|
476
|
+
(process.platform === "win32" ? join(tmpdir(), "harnery-admission") : "/tmp/harnery-admission")
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Linux memory pressure: the `full avg10` figure from /proc/pressure/memory,
|
|
482
|
+
* the share of the last ten seconds in which every runnable task stalled on
|
|
483
|
+
* memory. Undefined where pressure stall information is unavailable.
|
|
484
|
+
*/
|
|
485
|
+
export function memoryPressureFullAvg10(): number | undefined {
|
|
486
|
+
try {
|
|
487
|
+
const line = readFileSync("/proc/pressure/memory", "utf8")
|
|
488
|
+
.split("\n")
|
|
489
|
+
.find((candidate) => candidate.startsWith("full"));
|
|
490
|
+
const match = line ? /avg10=([0-9.]+)/.exec(line) : null;
|
|
491
|
+
return match?.[1] !== undefined ? Number(match[1]) : undefined;
|
|
492
|
+
} catch {
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export interface PressureWaitOptions {
|
|
498
|
+
/** Defer while `full avg10` is above this value. */
|
|
499
|
+
threshold: number;
|
|
500
|
+
/** Give up deferring after this long and let the caller start anyway. */
|
|
501
|
+
maxWaitMs: number;
|
|
502
|
+
pollMs?: number;
|
|
503
|
+
/** Pressure reader; tests inject one. */
|
|
504
|
+
read?: () => number | undefined;
|
|
505
|
+
/** Called once when deferral starts and once if it gives up. */
|
|
506
|
+
onWait?: (event: { pressure: number; waitedMs: number; gaveUp: boolean }) => void;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Wait, holding no slot, while the machine is already stalling on memory, so
|
|
511
|
+
* a heavy job does not start into a storm. Never blocks forever: after
|
|
512
|
+
* maxWaitMs it returns and the caller starts anyway. Returns the time waited.
|
|
513
|
+
*/
|
|
514
|
+
export async function waitForMemoryPressure(options: PressureWaitOptions): Promise<number> {
|
|
515
|
+
const read = options.read ?? memoryPressureFullAvg10;
|
|
516
|
+
const pollMs = options.pollMs ?? 5_000;
|
|
517
|
+
const started = Date.now();
|
|
518
|
+
let announced = false;
|
|
519
|
+
while (true) {
|
|
520
|
+
const pressure = read();
|
|
521
|
+
if (pressure === undefined || pressure <= options.threshold) return Date.now() - started;
|
|
522
|
+
const waitedMs = Date.now() - started;
|
|
523
|
+
if (waitedMs + pollMs > options.maxWaitMs) {
|
|
524
|
+
options.onWait?.({ pressure, waitedMs, gaveUp: true });
|
|
525
|
+
return waitedMs;
|
|
526
|
+
}
|
|
527
|
+
if (!announced) {
|
|
528
|
+
announced = true;
|
|
529
|
+
options.onWait?.({ pressure, waitedMs, gaveUp: false });
|
|
530
|
+
}
|
|
531
|
+
await sleep(pollMs);
|
|
532
|
+
}
|
|
335
533
|
}
|
|
336
534
|
|
|
337
535
|
/** Resources present under an admission base directory. */
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type CookieJar,
|
|
16
16
|
CookieStoreParseError,
|
|
17
17
|
type Cookie as JarCookie,
|
|
18
|
+
violatesCookiePrefix,
|
|
18
19
|
} from "../cookies/index.ts";
|
|
19
20
|
import { PaceGate, pacePolicyFromEnv } from "../pace/index.ts";
|
|
20
21
|
import { type AssertResult, type AssertSpec, buildAssertCheck } from "./asserts.js";
|
|
@@ -170,8 +171,22 @@ export interface BrowserOptions {
|
|
|
170
171
|
userAgent?: string;
|
|
171
172
|
/** Authenticated browser proxy passed directly to Playwright. */
|
|
172
173
|
proxy?: { server: string; username?: string; password?: string };
|
|
174
|
+
/**
|
|
175
|
+
* Inspect the live cookies of the browser context. Runs before each request
|
|
176
|
+
* is sent, after each response, and once more at close. If it throws, the
|
|
177
|
+
* request is cancelled, the context closes without saving cookies to the
|
|
178
|
+
* jar, and the error is kept as `cookieGuardFault` for the caller to report.
|
|
179
|
+
*/
|
|
180
|
+
cookieGuard?: CookieGuard;
|
|
173
181
|
}
|
|
174
182
|
|
|
183
|
+
export type CookieGuardPhase = "request" | "response" | "close";
|
|
184
|
+
|
|
185
|
+
export type CookieGuard = (
|
|
186
|
+
cookies: readonly PWCookie[],
|
|
187
|
+
event: { phase: CookieGuardPhase; url: string | null },
|
|
188
|
+
) => void | Promise<void>;
|
|
189
|
+
|
|
175
190
|
export interface NavigateResult {
|
|
176
191
|
url: string;
|
|
177
192
|
title: string;
|
|
@@ -422,16 +437,35 @@ export class Browser {
|
|
|
422
437
|
: undefined;
|
|
423
438
|
if (identity) await this.context.addInitScript(installDocumentIdentity, identity);
|
|
424
439
|
|
|
425
|
-
|
|
426
|
-
|
|
440
|
+
// The jar's domain field is the delivery rule: a dotless domain is
|
|
441
|
+
// host-only, which Chromium honors for exactly that host. A cookie that
|
|
442
|
+
// breaks its name prefix would make Chromium reject the whole batch, so
|
|
443
|
+
// it is left out rather than aborting the session.
|
|
444
|
+
const injectable = jarCookies.filter((cookie) => !violatesCookiePrefix(cookie));
|
|
445
|
+
if (injectable.length > 0) {
|
|
446
|
+
await this.context.addCookies(injectable.map(toPWCookie));
|
|
427
447
|
}
|
|
428
448
|
|
|
429
449
|
// Gate actual document requests, including links, forms, script navigation,
|
|
430
450
|
// and the first request of a popup. Method-level waits miss those loads.
|
|
431
451
|
// Redirect hops are part of the original load, not additional reservations.
|
|
432
452
|
const headersCb = this.opts.extraHeaders;
|
|
433
|
-
|
|
453
|
+
const cookieGuard = this.opts.cookieGuard;
|
|
454
|
+
if (cookieGuard) {
|
|
455
|
+
this.context.on("response", (response) => {
|
|
456
|
+
void this.guardCookies("response", response.url()).catch(() => undefined);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (this.getPaceGate()?.policy.enabled || headersCb || this.opts.userAgent || cookieGuard) {
|
|
434
460
|
await this.context.route("**/*", async (route, request) => {
|
|
461
|
+
if (cookieGuard) {
|
|
462
|
+
try {
|
|
463
|
+
await this.guardCookies("request", request.url());
|
|
464
|
+
} catch {
|
|
465
|
+
await route.abort("failed").catch(() => undefined);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
435
469
|
let mainDocument = false;
|
|
436
470
|
if (request.isNavigationRequest() && !request.redirectedFrom()) {
|
|
437
471
|
try {
|
|
@@ -848,24 +882,31 @@ export class Browser {
|
|
|
848
882
|
`captureRegionByScroll: rect must have a positive size (got ${rect.width}×${rect.height})`,
|
|
849
883
|
);
|
|
850
884
|
}
|
|
851
|
-
const settle = (top: number) =>
|
|
852
|
-
page.evaluate(
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
last =
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
885
|
+
const settle = (top: number, left?: number) =>
|
|
886
|
+
page.evaluate(
|
|
887
|
+
async (target: { top: number; left?: number }) => {
|
|
888
|
+
window.scrollTo({
|
|
889
|
+
left: target.left ?? window.scrollX,
|
|
890
|
+
top: target.top,
|
|
891
|
+
behavior: "instant",
|
|
892
|
+
});
|
|
893
|
+
const frame = () => new Promise<void>((r) => requestAnimationFrame(() => r()));
|
|
894
|
+
let last = Number.NaN;
|
|
895
|
+
for (let i = 0; i < 8; i++) {
|
|
896
|
+
await frame();
|
|
897
|
+
const now = window.scrollY;
|
|
898
|
+
if (now === last) break;
|
|
899
|
+
last = now;
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
scrollX: window.scrollX,
|
|
903
|
+
scrollY: window.scrollY,
|
|
904
|
+
innerWidth: window.innerWidth,
|
|
905
|
+
innerHeight: window.innerHeight,
|
|
906
|
+
};
|
|
907
|
+
},
|
|
908
|
+
{ top, left },
|
|
909
|
+
);
|
|
869
910
|
const original = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
|
870
911
|
const captures: Buffer[] = [];
|
|
871
912
|
// Viewport-pinned candidates, measured at scroll 0 so each later capture
|
|
@@ -884,6 +925,8 @@ export class Browser {
|
|
|
884
925
|
priorOpacityPriority: string;
|
|
885
926
|
priorTransition: string;
|
|
886
927
|
priorTransitionPriority: string;
|
|
928
|
+
priorStyle: string | null;
|
|
929
|
+
appliedStyle?: string;
|
|
887
930
|
}> = [];
|
|
888
931
|
// Fixed controls can live inside a static shadow host. Walk open roots
|
|
889
932
|
// without hiding their ordinary content or exceeding the total node cap.
|
|
@@ -907,6 +950,7 @@ export class Browser {
|
|
|
907
950
|
priorOpacityPriority: el.style.getPropertyPriority("opacity"),
|
|
908
951
|
priorTransition: el.style.getPropertyValue("transition"),
|
|
909
952
|
priorTransitionPriority: el.style.getPropertyPriority("transition"),
|
|
953
|
+
priorStyle: el.getAttribute("style"),
|
|
910
954
|
});
|
|
911
955
|
}
|
|
912
956
|
return out;
|
|
@@ -916,16 +960,36 @@ export class Browser {
|
|
|
916
960
|
async ([entries, action]) => {
|
|
917
961
|
for (const entry of entries) {
|
|
918
962
|
const restore = () => {
|
|
963
|
+
if (entry.appliedStyle === undefined) return;
|
|
964
|
+
const unchanged = entry.el.getAttribute("style") === entry.appliedStyle;
|
|
919
965
|
// Restore opacity while transitions are still disabled. Restoring both
|
|
920
966
|
// declarations in one style flush starts a new fade from zero.
|
|
921
|
-
|
|
922
|
-
|
|
967
|
+
if (
|
|
968
|
+
entry.el.style.getPropertyValue("opacity") === "0" &&
|
|
969
|
+
entry.el.style.getPropertyPriority("opacity") === "important"
|
|
970
|
+
)
|
|
971
|
+
entry.el.style.setProperty(
|
|
972
|
+
"opacity",
|
|
973
|
+
entry.priorOpacity,
|
|
974
|
+
entry.priorOpacityPriority,
|
|
975
|
+
);
|
|
923
976
|
void getComputedStyle(entry.el).opacity;
|
|
924
|
-
|
|
925
|
-
"transition"
|
|
926
|
-
entry.
|
|
927
|
-
|
|
928
|
-
|
|
977
|
+
if (
|
|
978
|
+
entry.el.style.getPropertyValue("transition") === "none" &&
|
|
979
|
+
entry.el.style.getPropertyPriority("transition") === "important"
|
|
980
|
+
)
|
|
981
|
+
entry.el.style.setProperty(
|
|
982
|
+
"transition",
|
|
983
|
+
entry.priorTransition,
|
|
984
|
+
entry.priorTransitionPriority,
|
|
985
|
+
);
|
|
986
|
+
// Preserve the exact original attribute, including its absence. Do
|
|
987
|
+
// not replace styles changed by the page while our override was active.
|
|
988
|
+
if (unchanged) {
|
|
989
|
+
if (entry.priorStyle === null) entry.el.removeAttribute("style");
|
|
990
|
+
else entry.el.setAttribute("style", entry.priorStyle);
|
|
991
|
+
}
|
|
992
|
+
entry.appliedStyle = undefined;
|
|
929
993
|
};
|
|
930
994
|
if (action === "restore") {
|
|
931
995
|
restore();
|
|
@@ -933,8 +997,21 @@ export class Browser {
|
|
|
933
997
|
}
|
|
934
998
|
const now = entry.el.getBoundingClientRect().top + window.scrollY;
|
|
935
999
|
if (Math.abs(now - entry.top) > 1) {
|
|
1000
|
+
if (entry.appliedStyle !== undefined) {
|
|
1001
|
+
if (entry.el.getAttribute("style") !== entry.appliedStyle)
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
"Pinned styles changed during native capture; source is unstable.",
|
|
1004
|
+
);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
entry.priorStyle = entry.el.getAttribute("style");
|
|
1008
|
+
entry.priorOpacity = entry.el.style.getPropertyValue("opacity");
|
|
1009
|
+
entry.priorOpacityPriority = entry.el.style.getPropertyPriority("opacity");
|
|
1010
|
+
entry.priorTransition = entry.el.style.getPropertyValue("transition");
|
|
1011
|
+
entry.priorTransitionPriority = entry.el.style.getPropertyPriority("transition");
|
|
936
1012
|
entry.el.style.setProperty("transition", "none", "important");
|
|
937
1013
|
entry.el.style.setProperty("opacity", "0", "important");
|
|
1014
|
+
entry.appliedStyle = entry.el.getAttribute("style")!;
|
|
938
1015
|
} else {
|
|
939
1016
|
restore();
|
|
940
1017
|
}
|
|
@@ -990,11 +1067,8 @@ export class Browser {
|
|
|
990
1067
|
await syncPinned("restore");
|
|
991
1068
|
} finally {
|
|
992
1069
|
await pinned.dispose();
|
|
993
|
-
await
|
|
994
|
-
|
|
995
|
-
window.scrollTo({ left: x, top: y, behavior: "instant" }),
|
|
996
|
-
original,
|
|
997
|
-
);
|
|
1070
|
+
await settle(original.y, original.x);
|
|
1071
|
+
await this.waitForReviewReady();
|
|
998
1072
|
}
|
|
999
1073
|
}
|
|
1000
1074
|
return captures;
|
|
@@ -1863,10 +1937,17 @@ export class Browser {
|
|
|
1863
1937
|
* visual line. Word-count per line via per-word Range rects (the width
|
|
1864
1938
|
* of the last line is deliberately NOT the signal; see runts.ts).
|
|
1865
1939
|
*/
|
|
1866
|
-
async checkRunts(
|
|
1940
|
+
async checkRunts(
|
|
1941
|
+
opts: { scope?: string | null; minChars?: number; maxFindings?: number } = {},
|
|
1942
|
+
): Promise<RuntsResult> {
|
|
1943
|
+
const maxFindings = opts.maxFindings === undefined ? 50 : opts.maxFindings;
|
|
1944
|
+
if (!Number.isInteger(maxFindings) || maxFindings < 1 || maxFindings > 5000) {
|
|
1945
|
+
throw new Error("maxFindings must be an integer from 1 to 5000.");
|
|
1946
|
+
}
|
|
1867
1947
|
return await this.currentPage.evaluate(buildRuntsCheck(), {
|
|
1868
1948
|
scope: opts.scope ?? null,
|
|
1869
1949
|
minChars: opts.minChars ?? 40,
|
|
1950
|
+
maxFindings,
|
|
1870
1951
|
});
|
|
1871
1952
|
}
|
|
1872
1953
|
|
|
@@ -1973,6 +2054,30 @@ export class Browser {
|
|
|
1973
2054
|
await this.context.addInitScript(script);
|
|
1974
2055
|
}
|
|
1975
2056
|
|
|
2057
|
+
/** The error a `cookieGuard` threw, once it has stopped the session. */
|
|
2058
|
+
get cookieGuardFault(): unknown {
|
|
2059
|
+
return this.cookieFault;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
private cookieFault: unknown;
|
|
2063
|
+
|
|
2064
|
+
/** Run the cookie guard; on a throw, record it, tear the context down, and rethrow. */
|
|
2065
|
+
private async guardCookies(phase: CookieGuardPhase, url: string | null): Promise<void> {
|
|
2066
|
+
const guard = this.opts.cookieGuard;
|
|
2067
|
+
const context = this.context;
|
|
2068
|
+
if (!guard || !context || this.cookieFault) {
|
|
2069
|
+
if (this.cookieFault) throw this.cookieFault;
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
try {
|
|
2073
|
+
await guard(await context.cookies(), { phase, url });
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
this.cookieFault ??= error;
|
|
2076
|
+
await this.close();
|
|
2077
|
+
throw error;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1976
2081
|
/** Snapshot all cookies in the live persistent context. */
|
|
1977
2082
|
async cookies(): Promise<PWCookie[]> {
|
|
1978
2083
|
if (!this.context) throw new Error("Browser not opened. Call open() first.");
|
|
@@ -1985,7 +2090,14 @@ export class Browser {
|
|
|
1985
2090
|
*/
|
|
1986
2091
|
async close(): Promise<void> {
|
|
1987
2092
|
if (!this.context) return;
|
|
1988
|
-
if (this.opts.
|
|
2093
|
+
if (this.opts.cookieGuard && !this.cookieFault) {
|
|
2094
|
+
try {
|
|
2095
|
+
await this.guardCookies("close", null);
|
|
2096
|
+
} catch {
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
if (this.opts.jar && !this.cookieFault) {
|
|
1989
2101
|
try {
|
|
1990
2102
|
const live = await this.context.cookies();
|
|
1991
2103
|
this.opts.jar.merge(live.map(toJarCookie));
|