feat: add PromoteToInteractive and DemoteToDocument for mid-session page transfer
Some checks failed
CI / build (pull_request) Successful in 29s
CI / test (pull_request) Successful in 36s
CI / vet (pull_request) Failing after 6m18s

Allow transferring ownership of a Playwright page between Document and
InteractiveBrowser modes without tearing down the browser. This enables
handing a live page to a human (e.g. for captcha solving) and resuming
scraping on the same page afterward.

Closes #76

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 02:27:42 +00:00
parent 39371dc261
commit e0da88b9b0
4 changed files with 155 additions and 22 deletions

65
promote.go Normal file
View File

@@ -0,0 +1,65 @@
package extractor
import "errors"
// ErrNotPromotable is returned when a Document cannot be promoted to an InteractiveBrowser.
// This happens when the Document is not backed by a Playwright page (e.g. a mock or custom implementation).
var ErrNotPromotable = errors.New("document is not promotable to InteractiveBrowser")
// ErrNotDemotable is returned when an InteractiveBrowser cannot be demoted to a Document.
// This happens when the InteractiveBrowser is not backed by a Playwright page.
var ErrNotDemotable = errors.New("interactive browser is not demotable to Document")
// ErrAlreadyDetached is returned when attempting to promote or demote an object that has
// already been transferred. Each Document or InteractiveBrowser can only be promoted/demoted once.
var ErrAlreadyDetached = errors.New("already detached")
// PromoteToInteractive transfers ownership of the underlying Playwright page from a Document
// to a new InteractiveBrowser. After promotion, the Document's Close method becomes a no-op
// (the page is now owned by the returned InteractiveBrowser).
//
// The caller must keep the original Browser alive while the promoted InteractiveBrowser is in use,
// since the Browser still owns the Playwright process and browser instance.
//
// Returns ErrNotPromotable if the Document is not backed by a Playwright page,
// or ErrAlreadyDetached if the Document was already promoted.
func PromoteToInteractive(doc Document) (InteractiveBrowser, error) {
d, ok := doc.(*document)
if !ok {
return nil, ErrNotPromotable
}
if d.detached {
return nil, ErrAlreadyDetached
}
d.detached = true
return &interactiveBrowser{
pw: d.pw,
browser: d.browser,
ctx: d.page.Context(),
page: d.page,
}, nil
}
// DemoteToDocument transfers ownership of the underlying Playwright page from an
// InteractiveBrowser back to a new Document. After demotion, the InteractiveBrowser's
// Close method becomes a no-op (the page is now owned by the returned Document).
//
// Returns ErrNotDemotable if the InteractiveBrowser is not backed by a Playwright page,
// or ErrAlreadyDetached if the InteractiveBrowser was already demoted.
func DemoteToDocument(ib InteractiveBrowser) (Document, error) {
b, ok := ib.(*interactiveBrowser)
if !ok {
return nil, ErrNotDemotable
}
if b.detached {
return nil, ErrAlreadyDetached
}
b.detached = true
return newDocument(b.pw, b.browser, b.page)
}