effect-browser 0.3.0-beta.19 → 0.3.0-beta.21
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/README.md +94 -3
- package/dist/Browser-CiXHT1yO.d.mts +129 -0
- package/dist/{Browser-H63wiekr.mjs → Browser-CxXUjxOp.mjs} +10 -19
- package/dist/Browser-CxXUjxOp.mjs.map +1 -0
- package/dist/Browser.d.mts +2 -129
- package/dist/Browser.mjs +1 -1
- package/dist/Cdp-CzCRnMlM.d.mts +10 -0
- package/dist/Cdp.d.mts +5 -26
- package/dist/Cdp.mjs +4 -45
- package/dist/Cdp.mjs.map +1 -1
- package/dist/Chromium.d.mts +1 -1
- package/dist/Chromium.mjs +3 -3
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Expect-u3P_ENLX.d.mts +62 -0
- package/dist/Expect.d.mts +2 -62
- package/dist/Expect.mjs +2 -2
- package/dist/LICENSE-jpeg-js +28 -0
- package/dist/Moment.mjs +1 -1
- package/dist/Page-BP02bmNv.mjs +264 -0
- package/dist/{Page-CfB6Zn1p.mjs.map → Page-BP02bmNv.mjs.map} +1 -1
- package/dist/Page.mjs +1 -1
- package/dist/{Plan-C-d5QYS8.mjs → Plan-BG1WifW2.mjs} +48 -4
- package/dist/Plan-BG1WifW2.mjs.map +1 -0
- package/dist/Plan.mjs +1 -1
- package/dist/Policy-BGWjbqCl.d.mts +76 -0
- package/dist/Policy.d.mts +2 -76
- package/dist/Policy.mjs +1 -1
- package/dist/Presentation-DxVikn9_.d.mts +48 -0
- package/dist/Presentation.d.mts +2 -48
- package/dist/Presentation.mjs +2 -2
- package/dist/Stage-NHp8N81k.d.mts +65 -0
- package/dist/Stage.d.mts +2 -65
- package/dist/Stage.mjs +2 -2
- package/dist/{Supervisor-mVPeEUp8.d.mts → Supervisor-CfRxbaso.d.mts} +2 -2
- package/dist/Supervisor.d.mts +1 -1
- package/dist/cdp-BMpYMWYe.mjs +69 -0
- package/dist/cdp-BMpYMWYe.mjs.map +1 -0
- package/dist/cdp-DL7XfEj5.d.mts +41 -0
- package/dist/{context-w3NJPRLo.mjs → context-EGhOqjNN.mjs} +5 -5
- package/dist/context-EGhOqjNN.mjs.map +1 -0
- package/dist/index.d.mts +7 -6
- package/dist/index.mjs +4 -3
- package/dist/page-PTM8m5wC.mjs +4057 -0
- package/dist/page-PTM8m5wC.mjs.map +1 -0
- package/dist/{url-Cd04Okid.mjs → url-Bsp2y55M.mjs} +2 -2
- package/dist/{url-Cd04Okid.mjs.map → url-Bsp2y55M.mjs.map} +1 -1
- package/dist/workerd/Cdp.d.mts +3 -0
- package/dist/workerd/Cdp.mjs +18 -0
- package/dist/workerd/Cdp.mjs.map +1 -0
- package/dist/workerd/index.d.mts +16 -0
- package/dist/workerd/index.mjs +17 -0
- package/package.json +15 -4
- package/src/Browser.ts +20 -30
- package/src/Cdp.ts +7 -92
- package/src/index.ts +1 -15
- package/src/internal/cdp.ts +134 -0
- package/src/internal/page/bridge.ts +2 -2
- package/src/internal/page/context.ts +21 -24
- package/src/internal/page/identity.ts +6 -25
- package/src/internal/pictures/motion.ts +3 -3
- package/src/workerd/Cdp.ts +11 -0
- package/src/workerd/index.ts +17 -0
- package/dist/Browser-H63wiekr.mjs.map +0 -1
- package/dist/Page-CfB6Zn1p.mjs +0 -610
- package/dist/Plan-C-d5QYS8.mjs.map +0 -1
- package/dist/context-w3NJPRLo.mjs.map +0 -1
- package/dist/match.inpage-xOe9LNO0.mjs +0 -48
- package/dist/match.inpage-xOe9LNO0.mjs.map +0 -1
- package/dist/page-BKwfF6z-.mjs +0 -5099
- package/dist/page-BKwfF6z-.mjs.map +0 -1
|
@@ -259,6 +259,6 @@ const quoted = /[a-z][a-z\d+.-]*:\/\/[^\s"'<>]+/gi;
|
|
|
259
259
|
/** `text` with each address in it redacted. */
|
|
260
260
|
const redactWithin = (text) => text.replace(quoted, (url) => redact(url));
|
|
261
261
|
//#endregion
|
|
262
|
-
export {
|
|
262
|
+
export { withheld as a, make as c, redactWithin as i, quoted as n, Correlation as o, redact as r, FailFast as s, parse as t };
|
|
263
263
|
|
|
264
|
-
//# sourceMappingURL=url-
|
|
264
|
+
//# sourceMappingURL=url-Bsp2y55M.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url-Cd04Okid.mjs","names":[],"sources":["../src/internal/page/lane.ts","../src/internal/page/url.ts"],"sourcesContent":["/**\n * A page's admission: the one lane every operation on the page takes its turn in, so a page waits\n * only for itself. A write, an action that sends input or navigates, holds the lane alone, in the\n * order writes asked; reads share it, after the write in flight and every write asked before them,\n * so a read describes the page an action left, never one an action is still changing. Each write\n * starts a new epoch as it asks. A turn not given in time fails `Busy`, never `Timeout`, and a\n * caller under `FailFast` gets it at once.\n *\n * Reads keep their work. An identical read asked in the same epoch and document joins the one in\n * flight, which runs in the page's scope under the action timeout whoever gives up; one that ends\n * after all its callers gave up serves the next to ask the same, within an action timeout. A write\n * stops the reads nobody awaits, since no caller after it could use them.\n *\n * Effect's `Semaphore` is not fair to a writer that needs every permit, so the rule is this\n * module's own: one state, changed by `step` alone, which the model test drives.\n */\nimport { Context, Deferred, Duration, Effect, Exit, type Fiber, type Scope } from \"effect\";\n\nimport { BrowserError, Busy } from \"../../BrowserError.ts\";\nimport type { Gate } from \"../input/gate.ts\";\n\nexport type Kind = \"read\" | \"write\";\n\nexport interface Ticket {\n readonly id: number;\n readonly kind: Kind;\n}\n\n/** Who holds the lane: no one, reads together, or one write. */\nexport type Turn =\n | { readonly _tag: \"Free\" }\n | { readonly _tag: \"Reading\"; readonly count: number }\n | { readonly _tag: \"Writing\" };\n\nexport interface State {\n readonly turn: Turn;\n /** The tickets waiting, in the order they asked. */\n readonly queue: ReadonlyArray<Ticket>;\n /** The writes asked so far: two reads in one epoch follow the same writes. */\n readonly epoch: number;\n}\n\nexport type Input =\n | { readonly _tag: \"Ask\"; readonly ticket: Ticket }\n /** The ticket gave up waiting, or ended its turn. */\n | { readonly _tag: \"Leave\"; readonly ticket: Ticket };\n\nexport const initial: State = { turn: { _tag: \"Free\" }, queue: [], epoch: 0 };\n\nconst holders = (turn: Turn) =>\n turn._tag === \"Reading\" ? turn.count : turn._tag === \"Writing\" ? 1 : 0;\n\n/** Whether a ticket of `kind` asking now would hold the lane at once. */\nexport const admitsNow = ({ turn, queue }: State, kind: Kind) =>\n queue.length === 0 && (kind === \"read\" ? turn._tag !== \"Writing\" : turn._tag === \"Free\");\n\n/**\n * The operations ahead of a waiting ticket: those holding the lane and those asked before it, or\n * all that are queued for a ticket not in the queue.\n */\nexport const ahead = ({ turn, queue }: State, id?: number) => {\n const at = queue.findIndex((ticket) => ticket.id === id);\n\n return holders(turn) + (at === -1 ? queue.length : at);\n};\n\n/**\n * The lane after `input`, and the tickets it lets in. Tickets go in from the head of the queue\n * only: a write once the lane is free, reads while no write holds it, so reads asked before a\n * write go before it, and reads asked after it wait for it.\n */\nexport const step = (\n state: State,\n input: Input,\n): { readonly state: State; readonly admitted: ReadonlyArray<Ticket> } => {\n let { turn, queue, epoch } = state;\n\n if (input._tag === \"Ask\") {\n queue = [...queue, input.ticket];\n if (input.ticket.kind === \"write\") epoch += 1;\n } else {\n const at = queue.findIndex((ticket) => ticket.id === input.ticket.id);\n\n if (at !== -1) queue = queue.toSpliced(at, 1);\n else\n turn =\n turn._tag === \"Reading\" && turn.count > 1\n ? { _tag: \"Reading\", count: turn.count - 1 }\n : { _tag: \"Free\" };\n }\n\n const admitted: Array<Ticket> = [];\n\n for (const ticket of queue) {\n if (ticket.kind === \"write\" ? turn._tag !== \"Free\" : turn._tag === \"Writing\") break;\n admitted.push(ticket);\n turn =\n ticket.kind === \"write\" ? { _tag: \"Writing\" } : { _tag: \"Reading\", count: holders(turn) + 1 };\n }\n\n return { state: { turn, queue: queue.slice(admitted.length), epoch }, admitted };\n};\n\n/** Whether operations on a busy page fail `Busy` at once rather than wait their turn. */\nexport const FailFast = Context.Reference<boolean>(\"effect-browser/internal/page/FailFast\", {\n defaultValue: () => false,\n});\n\n/** The id a caller gives the actions it asks for, which each records as its `correlation`. */\nexport const Correlation = Context.Reference<string | undefined>(\n \"effect-browser/internal/page/Correlation\",\n { defaultValue: () => undefined },\n);\n\n/** A shared read in flight: its epoch, its result once it ends, and how many callers await it. */\ninterface Flight<A> {\n readonly epoch: number;\n readonly result: Deferred.Deferred<A, BrowserError>;\n waiting: number;\n fiber: Fiber.Fiber<A, BrowserError> | undefined;\n}\n\nexport const make = (options: {\n readonly now: () => number;\n readonly actionTimeout: Duration.Duration;\n /** When the page's current document began: a read is shared only within one. */\n readonly documentAt: () => number;\n /** The failure of a read still awaited as the page closes. */\n readonly gone: (operation: string) => BrowserError;\n /** The page's scope, which shared reads run in. */\n readonly scope: Scope.Scope;\n readonly inputGate?: Gate | undefined;\n}) => {\n const { now, actionTimeout } = options;\n let state = initial;\n let tickets = 0;\n const admissions = new Map<number, Deferred.Deferred<void>>();\n // Each shared read's sweep, which a write runs as it asks.\n const sweeps: Array<() => void> = [];\n\n const apply = (input: Input) => {\n const next = step(state, input);\n\n state = next.state;\n if (input._tag === \"Leave\") admissions.delete(input.ticket.id);\n for (const ticket of next.admitted) {\n const admitted = admissions.get(ticket.id);\n\n admissions.delete(ticket.id);\n if (admitted !== undefined) Deferred.doneUnsafe(admitted, Exit.void);\n }\n if (input._tag === \"Ask\" && input.ticket.kind === \"write\") for (const sweep of sweeps) sweep();\n };\n\n const busy = (operation: string, askedAt: number, id?: number) =>\n new BrowserError({\n operation,\n reason: new Busy({ waitedMillis: Math.round(now() - askedAt), ahead: ahead(state, id) }),\n dispatched: false,\n });\n\n /**\n * Run `effect` in a turn of `kind`, waiting at most `wait` for it, or failing `Busy`. The turn\n * ends with `effect`, however it ends.\n */\n const turn =\n (kind: Kind, operation: string, wait: Duration.Duration) =>\n <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E | BrowserError, R> =>\n Effect.uninterruptibleMask((restore) =>\n Effect.gen(function* () {\n const ticket: Ticket = { id: ++tickets, kind };\n const askedAt = now();\n\n if (kind === \"write\" && options.inputGate !== undefined)\n yield* options.inputGate.check(operation);\n\n if ((yield* FailFast) && !admitsNow(state, kind)) return yield* busy(operation, askedAt);\n const admitted = Deferred.makeUnsafe<void>();\n\n admissions.set(ticket.id, admitted);\n apply({ _tag: \"Ask\", ticket });\n\n const waiting = Deferred.await(admitted).pipe(\n Effect.timeoutOrElse({\n duration: wait,\n orElse: () =>\n Deferred.isDoneUnsafe(admitted)\n ? Effect.void\n : Effect.fail(busy(operation, askedAt, ticket.id)),\n }),\n );\n\n const given = yield* Effect.exit(\n restore(\n kind === \"write\" && options.inputGate !== undefined\n ? Effect.raceFirst(waiting, options.inputGate.refused(operation))\n : waiting,\n ),\n );\n\n if (Exit.isFailure(given)) {\n apply({ _tag: \"Leave\", ticket });\n\n return yield* Effect.failCause(given.cause);\n }\n\n return yield* restore(effect).pipe(\n Effect.ensuring(Effect.sync(() => apply({ _tag: \"Leave\", ticket }))),\n );\n }),\n );\n\n /**\n * A read whose identical calls share their work: `work` runs once per epoch and document while\n * calls await it, and with `keep` its result serves the next call after all of them gave up,\n * within an action timeout of its end. Pictures, whose callers say how old they may be, only\n * join.\n */\n const shared = <A>(operation: string, keep: boolean) => {\n const flights = new Map<string, Flight<A>>();\n const kept = new Map<string, { readonly value: A; readonly until: number }>();\n\n sweeps.push(() => {\n kept.clear();\n for (const flight of flights.values())\n if (flight.waiting === 0) flight.fiber?.interruptUnsafe();\n });\n\n // A read in flight, its first caller waiting. However its fiber ends, even before it began,\n // as when the page closes, its callers hear.\n const start = (id: string, work: Effect.Effect<A, BrowserError>) =>\n Effect.gen(function* () {\n const flight: Flight<A> = {\n epoch: state.epoch,\n result: Deferred.makeUnsafe(),\n waiting: 1,\n fiber: undefined,\n };\n\n flights.set(id, flight);\n flight.fiber = yield* turn(\n \"read\",\n operation,\n actionTimeout,\n )(work).pipe(\n // The caller chose whether to wait; the work it started serves others too.\n Effect.provideService(FailFast, false),\n Effect.forkIn(options.scope),\n );\n flight.fiber.addObserver((exit) => {\n flights.delete(id);\n if (keep && Exit.isSuccess(exit) && flight.waiting === 0 && flight.epoch === state.epoch)\n kept.set(id, { value: exit.value, until: now() + Duration.toMillis(actionTimeout) });\n Deferred.doneUnsafe(\n flight.result,\n Exit.hasInterrupts(exit) ? Effect.fail(options.gone(operation)) : exit,\n );\n });\n\n return flight;\n });\n\n const join = (id: string, work: Effect.Effect<A, BrowserError>) =>\n Effect.acquireUseRelease(\n Effect.suspend(() => {\n const flying = flights.get(id);\n\n if (flying === undefined) return start(id, work);\n flying.waiting += 1;\n\n return Effect.as(Effect.annotateCurrentSpan(\"shared\", \"joined\"), flying);\n }),\n (flight) => Deferred.await(flight.result),\n (flight) =>\n Effect.sync(() => {\n flight.waiting -= 1;\n if (flight.waiting === 0 && flight.epoch < state.epoch) flight.fiber?.interruptUnsafe();\n }),\n );\n\n return (key: string, work: Effect.Effect<A, BrowserError>): Effect.Effect<A, BrowserError> =>\n Effect.gen(function* () {\n const id = `${state.epoch} ${options.documentAt()} ${key}`;\n const at = now();\n\n for (const [old, { until }] of kept) if (until < at) kept.delete(old);\n const hit = kept.get(id);\n\n if (hit !== undefined) {\n kept.delete(id);\n yield* Effect.annotateCurrentSpan(\"shared\", \"kept\");\n\n return hit.value;\n }\n if ((yield* FailFast) && !admitsNow(state, \"read\")) return yield* busy(operation, at);\n\n return yield* join(id, work);\n });\n };\n\n return {\n /** A turn alone, for an action: `wait` bounds the wait for it. */\n write: (operation: string, wait: Duration.Duration) => turn(\"write\", operation, wait),\n /** A turn beside other reads, waited for within the action timeout. */\n read: (operation: string, wait: Duration.Duration = actionTimeout) =>\n turn(\"read\", operation, wait),\n shared,\n };\n};\n\nexport type Lane = ReturnType<typeof make>;\n","/**\n * Addresses: reading one the way a person types it, for `Page.goto` and the browser tools alike,\n * and reporting one without the values that are credentials, by one rule that the page script runs\n * too, for the outline's links.\n */\n\nconst scheme = /^[a-z][a-z0-9+.-]*:/i;\n\n// `localhost:3000` and `example.com:8080/a` look like a scheme to the URL parser; a port decides.\nconst hostAndPort = /^[^\\s/?#@:]+:\\d+(?:[/?#]|$)/;\n\nconst loopback = /^(?:localhost|127(?:\\.\\d+){3}|\\[::1\\])(?:[:/?#]|$)/i;\n\n/** The URL `input` names. Without a scheme it is a web address: HTTPS, or HTTP on loopback. */\nexport const parse = (input: string): URL | null => {\n const text = input.trim();\n\n if (scheme.test(text) && !hostAndPort.test(text)) return URL.parse(text);\n\n return URL.parse(`${loopback.test(text) ? \"http\" : \"https\"}://${text}`);\n};\n\n/**\n * The rule that reports an address without its credentials. The page script runs it too, so it\n * stays self-contained, as `names.inpage.ts` says a page-side part must.\n */\nexport const addresses = () => {\n // What a credential's value reads as, as a secret field's text does (`Page.redacted`).\n const withheld = \"••••••••\" as const;\n\n // A parameter's name, compared in lowercase without separators, says it is a credential: a token,\n // secret, password, signature, assertion, session id or one-time code, or a code or key of a kind\n // that signs someone in or signs a request, such as `verification_code` or `api_key`.\n const credential =\n /token|secret|passw|passcode|passphrase|signature|credential|assertion|verifier|bearer|jwt|hmac|apikey|accesskey|privatekey|sess(?:ion)?id|saml(?:response|art)|(?:sig|otp)$|(?:auth|api|access|private|secret|signing|client|encryption|licen[cs]e|session|master|app|device|verification|verify|confirmation|confirm|activation|activate|reset|recovery|login|security|magic|onetime|mfa|2fa|sms|email|otp|oob)(?:code|key)$|^(?:auth|authorization|pwd|pass)$/;\n\n // These names as often say what an address is about, such as a stock code, a support ticket or a\n // conference session. Under them a value is a credential when it looks generated, at least 16\n // characters with letters and digits, as tokens, session ids and authorization codes do.\n const ambiguous = /^(?:code|key|session|sid|ticket)$/;\n\n const secret = (name: string, value: string) => {\n const bare = name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n\n return (\n value !== \"\" &&\n (credential.test(bare) ||\n (ambiguous.test(bare) && value.length >= 16 && /\\d/.test(value) && /[a-z]/i.test(value)))\n );\n };\n\n // Parameters as written, each credential's value withheld: a query's, a fragment's written as a\n // query, or a path's segments', as a Java session id is.\n const masked = (parameters: string, separator: string) =>\n parameters\n .split(separator)\n .map((parameter) => {\n const [name = \"\", value = \"\"] = [...new URLSearchParams(parameter)][0] ?? [];\n\n return secret(name, value)\n ? `${parameter.slice(0, parameter.indexOf(\"=\") + 1)}${withheld}`\n : parameter;\n })\n .join(separator);\n\n /**\n * `url` as the library reports it: without userinfo, and with each credential's value withheld,\n * so the rest keeps its identity, such as a chart's `?ticker=ETH`. An address without a host,\n * such as `data:` or `about:blank`, is content or a fixed name, and stays as it is.\n */\n const redact = (url: string): string => {\n const parsed = URL.parse(url);\n\n if (parsed === null || parsed.host === \"\") return url;\n parsed.username = \"\";\n parsed.password = \"\";\n parsed.pathname = masked(parsed.pathname, \";\");\n parsed.search = masked(parsed.search.slice(1), \"&\");\n if (parsed.hash.includes(\"=\")) {\n const fragment = parsed.hash.slice(1);\n const question = fragment.indexOf(\"?\");\n // A route before the parameters, as in `#/inbox?token=…`, stays.\n const start = question >= 0 && question < fragment.indexOf(\"=\") ? question + 1 : 0;\n\n parsed.hash = fragment.slice(0, start) + masked(fragment.slice(start), \"&\");\n }\n\n return parsed.href.replaceAll(encodeURIComponent(withheld), withheld);\n };\n\n return { withheld, redact };\n};\n\nexport const { withheld, redact } = addresses();\n\n/** An address inside text, as a failure's message quotes one. */\nexport const quoted = /[a-z][a-z\\d+.-]*:\\/\\/[^\\s\"'<>]+/gi;\n\n/** `text` with each address in it redacted. */\nexport const redactWithin = (text: string) => text.replace(quoted, (url) => redact(url));\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"url-Bsp2y55M.mjs","names":[],"sources":["../src/internal/page/lane.ts","../src/internal/page/url.ts"],"sourcesContent":["/**\n * A page's admission: the one lane every operation on the page takes its turn in, so a page waits\n * only for itself. A write, an action that sends input or navigates, holds the lane alone, in the\n * order writes asked; reads share it, after the write in flight and every write asked before them,\n * so a read describes the page an action left, never one an action is still changing. Each write\n * starts a new epoch as it asks. A turn not given in time fails `Busy`, never `Timeout`, and a\n * caller under `FailFast` gets it at once.\n *\n * Reads keep their work. An identical read asked in the same epoch and document joins the one in\n * flight, which runs in the page's scope under the action timeout whoever gives up; one that ends\n * after all its callers gave up serves the next to ask the same, within an action timeout. A write\n * stops the reads nobody awaits, since no caller after it could use them.\n *\n * Effect's `Semaphore` is not fair to a writer that needs every permit, so the rule is this\n * module's own: one state, changed by `step` alone, which the model test drives.\n */\nimport { Context, Deferred, Duration, Effect, Exit, type Fiber, type Scope } from \"effect\";\n\nimport { BrowserError, Busy } from \"../../BrowserError.ts\";\nimport type { Gate } from \"../input/gate.ts\";\n\nexport type Kind = \"read\" | \"write\";\n\nexport interface Ticket {\n readonly id: number;\n readonly kind: Kind;\n}\n\n/** Who holds the lane: no one, reads together, or one write. */\nexport type Turn =\n | { readonly _tag: \"Free\" }\n | { readonly _tag: \"Reading\"; readonly count: number }\n | { readonly _tag: \"Writing\" };\n\nexport interface State {\n readonly turn: Turn;\n /** The tickets waiting, in the order they asked. */\n readonly queue: ReadonlyArray<Ticket>;\n /** The writes asked so far: two reads in one epoch follow the same writes. */\n readonly epoch: number;\n}\n\nexport type Input =\n | { readonly _tag: \"Ask\"; readonly ticket: Ticket }\n /** The ticket gave up waiting, or ended its turn. */\n | { readonly _tag: \"Leave\"; readonly ticket: Ticket };\n\nexport const initial: State = { turn: { _tag: \"Free\" }, queue: [], epoch: 0 };\n\nconst holders = (turn: Turn) =>\n turn._tag === \"Reading\" ? turn.count : turn._tag === \"Writing\" ? 1 : 0;\n\n/** Whether a ticket of `kind` asking now would hold the lane at once. */\nexport const admitsNow = ({ turn, queue }: State, kind: Kind) =>\n queue.length === 0 && (kind === \"read\" ? turn._tag !== \"Writing\" : turn._tag === \"Free\");\n\n/**\n * The operations ahead of a waiting ticket: those holding the lane and those asked before it, or\n * all that are queued for a ticket not in the queue.\n */\nexport const ahead = ({ turn, queue }: State, id?: number) => {\n const at = queue.findIndex((ticket) => ticket.id === id);\n\n return holders(turn) + (at === -1 ? queue.length : at);\n};\n\n/**\n * The lane after `input`, and the tickets it lets in. Tickets go in from the head of the queue\n * only: a write once the lane is free, reads while no write holds it, so reads asked before a\n * write go before it, and reads asked after it wait for it.\n */\nexport const step = (\n state: State,\n input: Input,\n): { readonly state: State; readonly admitted: ReadonlyArray<Ticket> } => {\n let { turn, queue, epoch } = state;\n\n if (input._tag === \"Ask\") {\n queue = [...queue, input.ticket];\n if (input.ticket.kind === \"write\") epoch += 1;\n } else {\n const at = queue.findIndex((ticket) => ticket.id === input.ticket.id);\n\n if (at !== -1) queue = queue.toSpliced(at, 1);\n else\n turn =\n turn._tag === \"Reading\" && turn.count > 1\n ? { _tag: \"Reading\", count: turn.count - 1 }\n : { _tag: \"Free\" };\n }\n\n const admitted: Array<Ticket> = [];\n\n for (const ticket of queue) {\n if (ticket.kind === \"write\" ? turn._tag !== \"Free\" : turn._tag === \"Writing\") break;\n admitted.push(ticket);\n turn =\n ticket.kind === \"write\" ? { _tag: \"Writing\" } : { _tag: \"Reading\", count: holders(turn) + 1 };\n }\n\n return { state: { turn, queue: queue.slice(admitted.length), epoch }, admitted };\n};\n\n/** Whether operations on a busy page fail `Busy` at once rather than wait their turn. */\nexport const FailFast = Context.Reference<boolean>(\"effect-browser/internal/page/FailFast\", {\n defaultValue: () => false,\n});\n\n/** The id a caller gives the actions it asks for, which each records as its `correlation`. */\nexport const Correlation = Context.Reference<string | undefined>(\n \"effect-browser/internal/page/Correlation\",\n { defaultValue: () => undefined },\n);\n\n/** A shared read in flight: its epoch, its result once it ends, and how many callers await it. */\ninterface Flight<A> {\n readonly epoch: number;\n readonly result: Deferred.Deferred<A, BrowserError>;\n waiting: number;\n fiber: Fiber.Fiber<A, BrowserError> | undefined;\n}\n\nexport const make = (options: {\n readonly now: () => number;\n readonly actionTimeout: Duration.Duration;\n /** When the page's current document began: a read is shared only within one. */\n readonly documentAt: () => number;\n /** The failure of a read still awaited as the page closes. */\n readonly gone: (operation: string) => BrowserError;\n /** The page's scope, which shared reads run in. */\n readonly scope: Scope.Scope;\n readonly inputGate?: Gate | undefined;\n}) => {\n const { now, actionTimeout } = options;\n let state = initial;\n let tickets = 0;\n const admissions = new Map<number, Deferred.Deferred<void>>();\n // Each shared read's sweep, which a write runs as it asks.\n const sweeps: Array<() => void> = [];\n\n const apply = (input: Input) => {\n const next = step(state, input);\n\n state = next.state;\n if (input._tag === \"Leave\") admissions.delete(input.ticket.id);\n for (const ticket of next.admitted) {\n const admitted = admissions.get(ticket.id);\n\n admissions.delete(ticket.id);\n if (admitted !== undefined) Deferred.doneUnsafe(admitted, Exit.void);\n }\n if (input._tag === \"Ask\" && input.ticket.kind === \"write\") for (const sweep of sweeps) sweep();\n };\n\n const busy = (operation: string, askedAt: number, id?: number) =>\n new BrowserError({\n operation,\n reason: new Busy({ waitedMillis: Math.round(now() - askedAt), ahead: ahead(state, id) }),\n dispatched: false,\n });\n\n /**\n * Run `effect` in a turn of `kind`, waiting at most `wait` for it, or failing `Busy`. The turn\n * ends with `effect`, however it ends.\n */\n const turn =\n (kind: Kind, operation: string, wait: Duration.Duration) =>\n <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E | BrowserError, R> =>\n Effect.uninterruptibleMask((restore) =>\n Effect.gen(function* () {\n const ticket: Ticket = { id: ++tickets, kind };\n const askedAt = now();\n\n if (kind === \"write\" && options.inputGate !== undefined)\n yield* options.inputGate.check(operation);\n\n if ((yield* FailFast) && !admitsNow(state, kind)) return yield* busy(operation, askedAt);\n const admitted = Deferred.makeUnsafe<void>();\n\n admissions.set(ticket.id, admitted);\n apply({ _tag: \"Ask\", ticket });\n\n const waiting = Deferred.await(admitted).pipe(\n Effect.timeoutOrElse({\n duration: wait,\n orElse: () =>\n Deferred.isDoneUnsafe(admitted)\n ? Effect.void\n : Effect.fail(busy(operation, askedAt, ticket.id)),\n }),\n );\n\n const given = yield* Effect.exit(\n restore(\n kind === \"write\" && options.inputGate !== undefined\n ? Effect.raceFirst(waiting, options.inputGate.refused(operation))\n : waiting,\n ),\n );\n\n if (Exit.isFailure(given)) {\n apply({ _tag: \"Leave\", ticket });\n\n return yield* Effect.failCause(given.cause);\n }\n\n return yield* restore(effect).pipe(\n Effect.ensuring(Effect.sync(() => apply({ _tag: \"Leave\", ticket }))),\n );\n }),\n );\n\n /**\n * A read whose identical calls share their work: `work` runs once per epoch and document while\n * calls await it, and with `keep` its result serves the next call after all of them gave up,\n * within an action timeout of its end. Pictures, whose callers say how old they may be, only\n * join.\n */\n const shared = <A>(operation: string, keep: boolean) => {\n const flights = new Map<string, Flight<A>>();\n const kept = new Map<string, { readonly value: A; readonly until: number }>();\n\n sweeps.push(() => {\n kept.clear();\n for (const flight of flights.values())\n if (flight.waiting === 0) flight.fiber?.interruptUnsafe();\n });\n\n // A read in flight, its first caller waiting. However its fiber ends, even before it began,\n // as when the page closes, its callers hear.\n const start = (id: string, work: Effect.Effect<A, BrowserError>) =>\n Effect.gen(function* () {\n const flight: Flight<A> = {\n epoch: state.epoch,\n result: Deferred.makeUnsafe(),\n waiting: 1,\n fiber: undefined,\n };\n\n flights.set(id, flight);\n flight.fiber = yield* turn(\n \"read\",\n operation,\n actionTimeout,\n )(work).pipe(\n // The caller chose whether to wait; the work it started serves others too.\n Effect.provideService(FailFast, false),\n Effect.forkIn(options.scope),\n );\n flight.fiber.addObserver((exit) => {\n flights.delete(id);\n if (keep && Exit.isSuccess(exit) && flight.waiting === 0 && flight.epoch === state.epoch)\n kept.set(id, { value: exit.value, until: now() + Duration.toMillis(actionTimeout) });\n Deferred.doneUnsafe(\n flight.result,\n Exit.hasInterrupts(exit) ? Effect.fail(options.gone(operation)) : exit,\n );\n });\n\n return flight;\n });\n\n const join = (id: string, work: Effect.Effect<A, BrowserError>) =>\n Effect.acquireUseRelease(\n Effect.suspend(() => {\n const flying = flights.get(id);\n\n if (flying === undefined) return start(id, work);\n flying.waiting += 1;\n\n return Effect.as(Effect.annotateCurrentSpan(\"shared\", \"joined\"), flying);\n }),\n (flight) => Deferred.await(flight.result),\n (flight) =>\n Effect.sync(() => {\n flight.waiting -= 1;\n if (flight.waiting === 0 && flight.epoch < state.epoch) flight.fiber?.interruptUnsafe();\n }),\n );\n\n return (key: string, work: Effect.Effect<A, BrowserError>): Effect.Effect<A, BrowserError> =>\n Effect.gen(function* () {\n const id = `${state.epoch} ${options.documentAt()} ${key}`;\n const at = now();\n\n for (const [old, { until }] of kept) if (until < at) kept.delete(old);\n const hit = kept.get(id);\n\n if (hit !== undefined) {\n kept.delete(id);\n yield* Effect.annotateCurrentSpan(\"shared\", \"kept\");\n\n return hit.value;\n }\n if ((yield* FailFast) && !admitsNow(state, \"read\")) return yield* busy(operation, at);\n\n return yield* join(id, work);\n });\n };\n\n return {\n /** A turn alone, for an action: `wait` bounds the wait for it. */\n write: (operation: string, wait: Duration.Duration) => turn(\"write\", operation, wait),\n /** A turn beside other reads, waited for within the action timeout. */\n read: (operation: string, wait: Duration.Duration = actionTimeout) =>\n turn(\"read\", operation, wait),\n shared,\n };\n};\n\nexport type Lane = ReturnType<typeof make>;\n","/**\n * Addresses: reading one the way a person types it, for `Page.goto` and the browser tools alike,\n * and reporting one without the values that are credentials, by one rule that the page script runs\n * too, for the outline's links.\n */\n\nconst scheme = /^[a-z][a-z0-9+.-]*:/i;\n\n// `localhost:3000` and `example.com:8080/a` look like a scheme to the URL parser; a port decides.\nconst hostAndPort = /^[^\\s/?#@:]+:\\d+(?:[/?#]|$)/;\n\nconst loopback = /^(?:localhost|127(?:\\.\\d+){3}|\\[::1\\])(?:[:/?#]|$)/i;\n\n/** The URL `input` names. Without a scheme it is a web address: HTTPS, or HTTP on loopback. */\nexport const parse = (input: string): URL | null => {\n const text = input.trim();\n\n if (scheme.test(text) && !hostAndPort.test(text)) return URL.parse(text);\n\n return URL.parse(`${loopback.test(text) ? \"http\" : \"https\"}://${text}`);\n};\n\n/**\n * The rule that reports an address without its credentials. The page script runs it too, so it\n * stays self-contained, as `names.inpage.ts` says a page-side part must.\n */\nexport const addresses = () => {\n // What a credential's value reads as, as a secret field's text does (`Page.redacted`).\n const withheld = \"••••••••\" as const;\n\n // A parameter's name, compared in lowercase without separators, says it is a credential: a token,\n // secret, password, signature, assertion, session id or one-time code, or a code or key of a kind\n // that signs someone in or signs a request, such as `verification_code` or `api_key`.\n const credential =\n /token|secret|passw|passcode|passphrase|signature|credential|assertion|verifier|bearer|jwt|hmac|apikey|accesskey|privatekey|sess(?:ion)?id|saml(?:response|art)|(?:sig|otp)$|(?:auth|api|access|private|secret|signing|client|encryption|licen[cs]e|session|master|app|device|verification|verify|confirmation|confirm|activation|activate|reset|recovery|login|security|magic|onetime|mfa|2fa|sms|email|otp|oob)(?:code|key)$|^(?:auth|authorization|pwd|pass)$/;\n\n // These names as often say what an address is about, such as a stock code, a support ticket or a\n // conference session. Under them a value is a credential when it looks generated, at least 16\n // characters with letters and digits, as tokens, session ids and authorization codes do.\n const ambiguous = /^(?:code|key|session|sid|ticket)$/;\n\n const secret = (name: string, value: string) => {\n const bare = name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n\n return (\n value !== \"\" &&\n (credential.test(bare) ||\n (ambiguous.test(bare) && value.length >= 16 && /\\d/.test(value) && /[a-z]/i.test(value)))\n );\n };\n\n // Parameters as written, each credential's value withheld: a query's, a fragment's written as a\n // query, or a path's segments', as a Java session id is.\n const masked = (parameters: string, separator: string) =>\n parameters\n .split(separator)\n .map((parameter) => {\n const [name = \"\", value = \"\"] = [...new URLSearchParams(parameter)][0] ?? [];\n\n return secret(name, value)\n ? `${parameter.slice(0, parameter.indexOf(\"=\") + 1)}${withheld}`\n : parameter;\n })\n .join(separator);\n\n /**\n * `url` as the library reports it: without userinfo, and with each credential's value withheld,\n * so the rest keeps its identity, such as a chart's `?ticker=ETH`. An address without a host,\n * such as `data:` or `about:blank`, is content or a fixed name, and stays as it is.\n */\n const redact = (url: string): string => {\n const parsed = URL.parse(url);\n\n if (parsed === null || parsed.host === \"\") return url;\n parsed.username = \"\";\n parsed.password = \"\";\n parsed.pathname = masked(parsed.pathname, \";\");\n parsed.search = masked(parsed.search.slice(1), \"&\");\n if (parsed.hash.includes(\"=\")) {\n const fragment = parsed.hash.slice(1);\n const question = fragment.indexOf(\"?\");\n // A route before the parameters, as in `#/inbox?token=…`, stays.\n const start = question >= 0 && question < fragment.indexOf(\"=\") ? question + 1 : 0;\n\n parsed.hash = fragment.slice(0, start) + masked(fragment.slice(start), \"&\");\n }\n\n return parsed.href.replaceAll(encodeURIComponent(withheld), withheld);\n };\n\n return { withheld, redact };\n};\n\nexport const { withheld, redact } = addresses();\n\n/** An address inside text, as a failure's message quotes one. */\nexport const quoted = /[a-z][a-z\\d+.-]*:\\/\\/[^\\s\"'<>]+/gi;\n\n/** `text` with each address in it redacted. */\nexport const redactWithin = (text: string) => text.replace(quoted, (url) => redact(url));\n"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { t as make } from "../cdp-BMpYMWYe.mjs";
|
|
3
|
+
//#region src/workerd/Cdp.ts
|
|
4
|
+
/**
|
|
5
|
+
* CDP lifetime for Workers. The consumer supplies a runtime-compatible Playwright connector;
|
|
6
|
+
* this entry imports no Playwright Node transport.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.3.0
|
|
9
|
+
*/
|
|
10
|
+
var Cdp_exports = /* @__PURE__ */ __exportAll({
|
|
11
|
+
layer: () => layer,
|
|
12
|
+
open: () => open
|
|
13
|
+
});
|
|
14
|
+
const { open, layer } = make();
|
|
15
|
+
//#endregion
|
|
16
|
+
export { layer, open, Cdp_exports as t };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=Cdp.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Cdp.mjs","names":[],"sources":["../../src/workerd/Cdp.ts"],"sourcesContent":["/**\n * CDP lifetime for Workers. The consumer supplies a runtime-compatible Playwright connector;\n * this entry imports no Playwright Node transport.\n *\n * @since 0.3.0\n */\nimport { make } from \"../internal/cdp.ts\";\n\nexport type { Connector, ConnectOptions, Options } from \"../internal/cdp.ts\";\n\nexport const { open, layer } = make();\n"],"mappings":";;;;;;;;;;;;;AAUA,MAAa,EAAE,MAAM,UAAU,KAAK"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { n as BrowserError_d_exports } from "../BrowserError-Coy7EW1N.mjs";
|
|
2
|
+
import { a as BrowserEvent_d_exports } from "../BrowserEvent-dq6FRT8w.mjs";
|
|
3
|
+
import { n as Change_d_exports } from "../Change-DqING3tr.mjs";
|
|
4
|
+
import { i as Frame_d_exports } from "../Frame-DkNdjjjr.mjs";
|
|
5
|
+
import { i as Snapshot_d_exports } from "../Snapshot-B34hZ4Jl.mjs";
|
|
6
|
+
import { N as Moment_d_exports, d as Page_d_exports } from "../Page-ku6j5t-v.mjs";
|
|
7
|
+
import { t as Motion_d_exports } from "../Motion-DND6I-64.mjs";
|
|
8
|
+
import { n as Browser_d_exports } from "../Browser-CiXHT1yO.mjs";
|
|
9
|
+
import { n as Expect_d_exports } from "../Expect-u3P_ENLX.mjs";
|
|
10
|
+
import { r as Plan_d_exports } from "../Plan-DJ4hEEna.mjs";
|
|
11
|
+
import { i as Policy_d_exports } from "../Policy-BGWjbqCl.mjs";
|
|
12
|
+
import { r as Presentation_d_exports } from "../Presentation-DxVikn9_.mjs";
|
|
13
|
+
import { o as Stage_d_exports } from "../Stage-NHp8N81k.mjs";
|
|
14
|
+
import { l as Supervisor_d_exports } from "../Supervisor-CfRxbaso.mjs";
|
|
15
|
+
import { t as Cdp_d_exports } from "../Cdp-CzCRnMlM.mjs";
|
|
16
|
+
export { Browser_d_exports as Browser, BrowserError_d_exports as BrowserError, BrowserEvent_d_exports as BrowserEvent, Cdp_d_exports as Cdp, Change_d_exports as Change, Expect_d_exports as Expect, Frame_d_exports as Frame, Moment_d_exports as Moment, Motion_d_exports as Motion, Page_d_exports as Page, Plan_d_exports as Plan, Policy_d_exports as Policy, Presentation_d_exports as Presentation, Snapshot_d_exports as Snapshot, Stage_d_exports as Stage, Supervisor_d_exports as Supervisor };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { t as Motion_exports } from "../Motion.mjs";
|
|
2
|
+
import { t as BrowserEvent_exports } from "../BrowserEvent.mjs";
|
|
3
|
+
import { t as BrowserError_exports } from "../BrowserError.mjs";
|
|
4
|
+
import { n as Browser_exports } from "../Browser-CxXUjxOp.mjs";
|
|
5
|
+
import { t as Frame_exports } from "../Frame.mjs";
|
|
6
|
+
import { a as Page_exports } from "../Page-BP02bmNv.mjs";
|
|
7
|
+
import { t as Snapshot_exports } from "../Snapshot.mjs";
|
|
8
|
+
import { r as Plan_exports } from "../Plan-BG1WifW2.mjs";
|
|
9
|
+
import { n as Change_exports } from "../Change-DNvztNLu.mjs";
|
|
10
|
+
import { t as Moment_exports } from "../Moment.mjs";
|
|
11
|
+
import { t as Expect_exports } from "../Expect.mjs";
|
|
12
|
+
import { t as Policy_exports } from "../Policy.mjs";
|
|
13
|
+
import { t as Presentation_exports } from "../Presentation.mjs";
|
|
14
|
+
import { t as Stage_exports } from "../Stage.mjs";
|
|
15
|
+
import { i as Supervisor_exports } from "../Supervisor-2mPjIYYs.mjs";
|
|
16
|
+
import { t as Cdp_exports } from "./Cdp.mjs";
|
|
17
|
+
export { Browser_exports as Browser, BrowserError_exports as BrowserError, BrowserEvent_exports as BrowserEvent, Cdp_exports as Cdp, Change_exports as Change, Expect_exports as Expect, Frame_exports as Frame, Moment_exports as Moment, Motion_exports as Motion, Page_exports as Page, Plan_exports as Plan, Policy_exports as Policy, Presentation_exports as Presentation, Snapshot_exports as Snapshot, Stage_exports as Stage, Supervisor_exports as Supervisor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-browser",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.21",
|
|
4
4
|
"description": "Effect browser sessions, page control, screencast and moments over Playwright and CDP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
"sideEffects": [],
|
|
17
17
|
"exports": {
|
|
18
18
|
".": {
|
|
19
|
+
"workerd": {
|
|
20
|
+
"@effect-browser/source": "./src/workerd/index.ts",
|
|
21
|
+
"types": "./dist/workerd/index.d.mts",
|
|
22
|
+
"default": "./dist/workerd/index.mjs"
|
|
23
|
+
},
|
|
19
24
|
"@effect-browser/source": "./src/index.ts",
|
|
20
25
|
"types": "./dist/index.d.mts",
|
|
21
26
|
"default": "./dist/index.mjs"
|
|
@@ -41,6 +46,11 @@
|
|
|
41
46
|
"default": "./dist/Change.mjs"
|
|
42
47
|
},
|
|
43
48
|
"./Cdp": {
|
|
49
|
+
"workerd": {
|
|
50
|
+
"@effect-browser/source": "./src/workerd/Cdp.ts",
|
|
51
|
+
"types": "./dist/workerd/Cdp.d.mts",
|
|
52
|
+
"default": "./dist/workerd/Cdp.mjs"
|
|
53
|
+
},
|
|
44
54
|
"@effect-browser/source": "./src/Cdp.ts",
|
|
45
55
|
"types": "./dist/Cdp.d.mts",
|
|
46
56
|
"default": "./dist/Cdp.mjs"
|
|
@@ -115,9 +125,10 @@
|
|
|
115
125
|
"test": "vp test --run"
|
|
116
126
|
},
|
|
117
127
|
"devDependencies": {
|
|
118
|
-
"@effect/vitest": "4.0.
|
|
128
|
+
"@effect/vitest": "4.0.3",
|
|
119
129
|
"@types/node": "26.1.2",
|
|
120
|
-
"effect": "4.0.
|
|
130
|
+
"effect": "4.0.3",
|
|
131
|
+
"esbuild": "0.28.2",
|
|
121
132
|
"playwright-core": "1.63.0",
|
|
122
133
|
"typescript": "7.0.2",
|
|
123
134
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
|
|
@@ -125,7 +136,7 @@
|
|
|
125
136
|
"vitest": "5.0.3"
|
|
126
137
|
},
|
|
127
138
|
"peerDependencies": {
|
|
128
|
-
"effect": "~4.0.
|
|
139
|
+
"effect": "~4.0.3",
|
|
129
140
|
"playwright-core": "^1.63.0"
|
|
130
141
|
},
|
|
131
142
|
"engines": {
|
package/src/Browser.ts
CHANGED
|
@@ -26,17 +26,7 @@ import {
|
|
|
26
26
|
import type { BrowserContext, Dialog, Page as PlaywrightPage } from "playwright-core";
|
|
27
27
|
|
|
28
28
|
import { type BrowserError, Failed, InvalidRequest, Limit } from "./BrowserError.ts";
|
|
29
|
-
import
|
|
30
|
-
type BrowserEvent,
|
|
31
|
-
DialogShown,
|
|
32
|
-
Disconnected,
|
|
33
|
-
type DisconnectCause,
|
|
34
|
-
PageClosed,
|
|
35
|
-
PageOpened,
|
|
36
|
-
PageUntracked,
|
|
37
|
-
type RecordedEvent,
|
|
38
|
-
SessionEnding,
|
|
39
|
-
} from "./BrowserEvent.ts";
|
|
29
|
+
import * as Events from "./BrowserEvent.ts";
|
|
40
30
|
import * as Gate from "./internal/input/gate.ts";
|
|
41
31
|
import { call, failWith, type Settings, undispatched } from "./internal/page/context.ts";
|
|
42
32
|
import * as Identity from "./internal/page/identity.ts";
|
|
@@ -167,11 +157,11 @@ export interface Service {
|
|
|
167
157
|
* Completes once the browser is lost to its owner, with why: its connection dropped, its
|
|
168
158
|
* provider ended its session, or its owner's scope released it.
|
|
169
159
|
*/
|
|
170
|
-
readonly disconnected: Effect.Effect<DisconnectCause>;
|
|
160
|
+
readonly disconnected: Effect.Effect<Events.DisconnectCause>;
|
|
171
161
|
/** Live events, or replay after a cursor. A reader that falls behind fails with EventHistoryExpired. */
|
|
172
|
-
readonly events: (options?: EventOptions) => Stream.Stream<RecordedEvent, BrowserError>;
|
|
162
|
+
readonly events: (options?: EventOptions) => Stream.Stream<Events.RecordedEvent, BrowserError>;
|
|
173
163
|
/** The latest `eventHistory` events, oldest first. */
|
|
174
|
-
readonly recentEvents: Effect.Effect<ReadonlyArray<BrowserEvent>>;
|
|
164
|
+
readonly recentEvents: Effect.Effect<ReadonlyArray<Events.BrowserEvent>>;
|
|
175
165
|
}
|
|
176
166
|
|
|
177
167
|
export class Browser extends Context.Service<Browser, Service>()("effect-browser/Browser") {}
|
|
@@ -230,14 +220,14 @@ const watchLoss = Effect.fnUntraced(function* (
|
|
|
230
220
|
context: BrowserContext,
|
|
231
221
|
expiresAt: DateTime.Utc | undefined,
|
|
232
222
|
clock: Clock.Clock,
|
|
233
|
-
disconnected: (cause: DisconnectCause) => void,
|
|
223
|
+
disconnected: (cause: Events.DisconnectCause) => void,
|
|
234
224
|
) {
|
|
235
|
-
let lostBy: DisconnectCause | undefined;
|
|
236
|
-
const lost = yield* Deferred.make<DisconnectCause>();
|
|
225
|
+
let lostBy: Events.DisconnectCause | undefined;
|
|
226
|
+
const lost = yield* Deferred.make<Events.DisconnectCause>();
|
|
237
227
|
const inFlight = new Set<(cause: Error) => void>();
|
|
238
228
|
const gone = () => new Error("Target page, context or browser has been closed");
|
|
239
229
|
|
|
240
|
-
const lose = (cause: DisconnectCause) => {
|
|
230
|
+
const lose = (cause: Events.DisconnectCause) => {
|
|
241
231
|
if (lostBy !== undefined) return;
|
|
242
232
|
lostBy = cause;
|
|
243
233
|
for (const reject of inFlight) reject(gone());
|
|
@@ -283,7 +273,7 @@ const watchLoss = Effect.fnUntraced(function* (
|
|
|
283
273
|
const listen = (
|
|
284
274
|
playwright: PlaywrightPage,
|
|
285
275
|
page: string,
|
|
286
|
-
publish: (event: BrowserEvent) => number,
|
|
276
|
+
publish: (event: Events.BrowserEvent) => number,
|
|
287
277
|
now: () => number,
|
|
288
278
|
on: { readonly close: () => void; readonly crash: () => void },
|
|
289
279
|
) => {
|
|
@@ -292,7 +282,7 @@ const listen = (
|
|
|
292
282
|
const answer = kind === "alert" || kind === "beforeunload" ? "accepted" : "dismissed";
|
|
293
283
|
const message = shown.message().slice(0, 500);
|
|
294
284
|
|
|
295
|
-
publish(new DialogShown({ at: now(), page, kind, message, answer }));
|
|
285
|
+
publish(new Events.DialogShown({ at: now(), page, kind, message, answer }));
|
|
296
286
|
(answer === "accepted" ? shown.accept() : shown.dismiss()).catch(() => undefined);
|
|
297
287
|
};
|
|
298
288
|
|
|
@@ -318,7 +308,7 @@ const listen = (
|
|
|
318
308
|
const followOpened = Effect.fnUntraced(function* (
|
|
319
309
|
context: BrowserContext,
|
|
320
310
|
register: (playwright: PlaywrightPage) => Effect.Effect<Page.Page, BrowserError>,
|
|
321
|
-
publish: (event: BrowserEvent) => number,
|
|
311
|
+
publish: (event: Events.BrowserEvent) => number,
|
|
322
312
|
now: () => number,
|
|
323
313
|
registrations: Openings.Registrations,
|
|
324
314
|
) {
|
|
@@ -334,7 +324,7 @@ const followOpened = Effect.fnUntraced(function* (
|
|
|
334
324
|
const url = Url.redact(playwright.url());
|
|
335
325
|
|
|
336
326
|
if (!playwright.isClosed())
|
|
337
|
-
publish(new PageUntracked({ at: now(), url, detail: error.message }));
|
|
327
|
+
publish(new Events.PageUntracked({ at: now(), url, detail: error.message }));
|
|
338
328
|
});
|
|
339
329
|
|
|
340
330
|
context.on("page", onPage);
|
|
@@ -368,7 +358,7 @@ const releaseNative = (clock: Clock.Clock) => (run: () => Promise<unknown>) =>
|
|
|
368
358
|
);
|
|
369
359
|
|
|
370
360
|
/** A page's own events among the browser's retained ones, and the openings of tabs it opened. */
|
|
371
|
-
const eventsOf = (recent: Effect.Effect<ReadonlyArray<RecordedEvent>>, page: string) =>
|
|
361
|
+
const eventsOf = (recent: Effect.Effect<ReadonlyArray<Events.RecordedEvent>>, page: string) =>
|
|
372
362
|
Effect.map(recent, (records) =>
|
|
373
363
|
records.flatMap(({ event }) =>
|
|
374
364
|
("page" in event && event.page === page) ||
|
|
@@ -448,21 +438,21 @@ const sentNow = <A>(sent: Promise<A>) => {
|
|
|
448
438
|
|
|
449
439
|
/** Close tab resources after publishing their close; a browser loss speaks for all its tabs. */
|
|
450
440
|
const closures = (
|
|
451
|
-
lostBy: () => DisconnectCause | undefined,
|
|
452
|
-
publish: (event: BrowserEvent) => number,
|
|
441
|
+
lostBy: () => Events.DisconnectCause | undefined,
|
|
442
|
+
publish: (event: Events.BrowserEvent) => number,
|
|
453
443
|
now: () => number,
|
|
454
444
|
) =>
|
|
455
445
|
Effect.gen(function* () {
|
|
456
446
|
const closing = yield* Queue.unbounded<{
|
|
457
447
|
readonly scope: Scope.Closeable;
|
|
458
448
|
readonly page: string;
|
|
459
|
-
readonly cause: PageClosed["cause"];
|
|
449
|
+
readonly cause: Events.PageClosed["cause"];
|
|
460
450
|
}>();
|
|
461
451
|
|
|
462
452
|
yield* Queue.take(closing).pipe(
|
|
463
453
|
Effect.tap(({ page, cause }) =>
|
|
464
454
|
Effect.sync(() => {
|
|
465
|
-
if (lostBy() === undefined) publish(new PageClosed({ at: now(), page, cause }));
|
|
455
|
+
if (lostBy() === undefined) publish(new Events.PageClosed({ at: now(), page, cause }));
|
|
466
456
|
}),
|
|
467
457
|
),
|
|
468
458
|
Effect.flatMap(({ scope }) => Scope.close(scope, Exit.void)),
|
|
@@ -503,13 +493,13 @@ export const make = Effect.fn("Browser.make")(function* (
|
|
|
503
493
|
const publish = timeline.publish;
|
|
504
494
|
|
|
505
495
|
if (info.expiresAt !== undefined)
|
|
506
|
-
publish(new SessionEnding({ at: now(), expiresAt: info.expiresAt }));
|
|
496
|
+
publish(new Events.SessionEnding({ at: now(), expiresAt: info.expiresAt }));
|
|
507
497
|
|
|
508
498
|
const { lostBy, lost, lose, untilLost, native } = yield* watchLoss(
|
|
509
499
|
context,
|
|
510
500
|
info.expiresAt,
|
|
511
501
|
clock,
|
|
512
|
-
(cause) => publish(new Disconnected({ at: now(), cause })),
|
|
502
|
+
(cause) => publish(new Events.Disconnected({ at: now(), cause })),
|
|
513
503
|
);
|
|
514
504
|
|
|
515
505
|
const inputGate = Gate.make({
|
|
@@ -598,7 +588,7 @@ export const make = Effect.fn("Browser.make")(function* (
|
|
|
598
588
|
|
|
599
589
|
registry.set(playwright, page);
|
|
600
590
|
publish(
|
|
601
|
-
new PageOpened({
|
|
591
|
+
new Events.PageOpened({
|
|
602
592
|
at: now(),
|
|
603
593
|
page: id,
|
|
604
594
|
url: Url.redact(targetInfo.url),
|
package/src/Cdp.ts
CHANGED
|
@@ -1,101 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A browser reached over
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Closing the scope disconnects. It does not close the remote browser; its owner does that.
|
|
2
|
+
* A browser reached over Chrome DevTools Protocol. Node defaults to Playwright's transport;
|
|
3
|
+
* `connect` supplies another runtime's real Playwright browser. Closing the scope disconnects.
|
|
6
4
|
*
|
|
7
5
|
* @since 0.3.0
|
|
8
6
|
*/
|
|
9
|
-
import { type DateTime, Effect, Layer, Redacted } from "effect";
|
|
10
7
|
import { chromium } from "playwright-core";
|
|
11
8
|
|
|
12
|
-
import
|
|
13
|
-
import { type BrowserError, Failed, type Reason } from "./BrowserError.ts";
|
|
14
|
-
import { reasonOf, undispatched } from "./internal/page/context.ts";
|
|
15
|
-
import * as Url from "./internal/page/url.ts";
|
|
16
|
-
|
|
17
|
-
export interface Options extends Browser.Options {
|
|
18
|
-
/**
|
|
19
|
-
* `ws://`, `wss://` or `http://` DevTools endpoint. Often a credential, so it may be redacted;
|
|
20
|
-
* connect failures never include its userinfo, path or query.
|
|
21
|
-
*/
|
|
22
|
-
readonly endpoint: string | Redacted.Redacted<string>;
|
|
23
|
-
readonly headers?: Readonly<Record<string, string>> | undefined;
|
|
24
|
-
/** Identifies the session in `Browser.id`. Defaults to `"cdp"`. */
|
|
25
|
-
readonly id?: string | undefined;
|
|
26
|
-
readonly provider?: string | undefined;
|
|
27
|
-
/** Bound on connecting. Defaults to 30 seconds. */
|
|
28
|
-
readonly connectTimeoutMillis?: number | undefined;
|
|
29
|
-
/** When the provider ends the session on its own, as a hosted session's timeout does. */
|
|
30
|
-
readonly expiresAt?: DateTime.Utc | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* A connection apart from this one for the pages' screencasts, as a hosted provider supplies.
|
|
33
|
-
* Without one, each page's capture runs on its own session.
|
|
34
|
-
*/
|
|
35
|
-
readonly capture?: Browser.CaptureSource | undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Playwright can echo the endpoint, or a URL derived from it with its query, userinfo or path, in
|
|
40
|
-
* a connect failure. Those parts are often the credential, so a URL keeps its scheme, host and
|
|
41
|
-
* port, and any other echo of the endpoint is replaced.
|
|
42
|
-
*/
|
|
43
|
-
const withoutEndpoint = (endpoint: string, reason: Reason): Reason => {
|
|
44
|
-
if (reason._tag !== "Failed") return reason;
|
|
45
|
-
|
|
46
|
-
const origins = reason.detail.replace(Url.quoted, (url) => {
|
|
47
|
-
const parsed = URL.parse(url);
|
|
48
|
-
|
|
49
|
-
return parsed === null || parsed.host === "" ? "<url>" : `${parsed.protocol}//${parsed.host}`;
|
|
50
|
-
});
|
|
9
|
+
import { make } from "./internal/cdp.ts";
|
|
51
10
|
|
|
52
|
-
|
|
53
|
-
detail: endpoint === "" ? origins : origins.replaceAll(endpoint, "<endpoint>"),
|
|
54
|
-
});
|
|
55
|
-
};
|
|
11
|
+
export type { Connector, ConnectOptions, Options } from "./internal/cdp.ts";
|
|
56
12
|
|
|
57
13
|
/** Connect and open a `Browser` over the endpoint's first context. */
|
|
58
|
-
export const open =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
: options.endpoint;
|
|
62
|
-
|
|
63
|
-
const timeout = options.connectTimeoutMillis ?? 30_000;
|
|
64
|
-
|
|
65
|
-
const failed = (cause: unknown, timeoutMillis?: number) =>
|
|
66
|
-
undispatched("connect", withoutEndpoint(endpoint, reasonOf(cause, timeoutMillis)));
|
|
67
|
-
|
|
68
|
-
// The endpoint is often a credential, so the connect span names no part of it.
|
|
69
|
-
const connected = yield* Effect.acquireRelease(
|
|
70
|
-
Effect.tryPromise({
|
|
71
|
-
try: () =>
|
|
72
|
-
chromium.connectOverCDP(endpoint, {
|
|
73
|
-
timeout,
|
|
74
|
-
...(options.headers === undefined ? {} : { headers: { ...options.headers } }),
|
|
75
|
-
}),
|
|
76
|
-
catch: (cause) => failed(cause, timeout),
|
|
77
|
-
}).pipe(Effect.withSpan("Cdp.connect", {}, { captureStackTrace: false })),
|
|
78
|
-
(browser) => Effect.tryPromise(() => browser.close()).pipe(Effect.ignore),
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const context =
|
|
82
|
-
connected.contexts()[0] ??
|
|
83
|
-
(yield* Effect.tryPromise({
|
|
84
|
-
try: () => connected.newContext(),
|
|
85
|
-
catch: (cause) => failed(cause),
|
|
86
|
-
}));
|
|
87
|
-
|
|
88
|
-
return yield* Browser.make(
|
|
89
|
-
context,
|
|
90
|
-
{
|
|
91
|
-
id: options.id ?? "cdp",
|
|
92
|
-
provider: options.provider ?? "cdp",
|
|
93
|
-
expiresAt: options.expiresAt,
|
|
94
|
-
capture: options.capture,
|
|
95
|
-
},
|
|
96
|
-
options,
|
|
97
|
-
);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
export const layer = (options: Options): Layer.Layer<Browser.Browser, BrowserError> =>
|
|
101
|
-
Layer.effect(Browser.Browser, open(options));
|
|
14
|
+
export const { open, layer } = make((endpoint, options) =>
|
|
15
|
+
chromium.connectOverCDP(endpoint, options),
|
|
16
|
+
);
|
package/src/index.ts
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @since 0.3.0
|
|
3
3
|
*/
|
|
4
|
-
export *
|
|
5
|
-
export * as BrowserError from "./BrowserError.ts";
|
|
6
|
-
export * as BrowserEvent from "./BrowserEvent.ts";
|
|
4
|
+
export * from "./workerd/index.ts";
|
|
7
5
|
export * as Cdp from "./Cdp.ts";
|
|
8
|
-
export * as Change from "./Change.ts";
|
|
9
6
|
export * as Chromium from "./Chromium.ts";
|
|
10
|
-
export * as Expect from "./Expect.ts";
|
|
11
|
-
export * as Frame from "./Frame.ts";
|
|
12
|
-
export * as Moment from "./Moment.ts";
|
|
13
|
-
export * as Motion from "./Motion.ts";
|
|
14
|
-
export * as Page from "./Page.ts";
|
|
15
|
-
export * as Plan from "./Plan.ts";
|
|
16
|
-
export * as Policy from "./Policy.ts";
|
|
17
|
-
export * as Presentation from "./Presentation.ts";
|
|
18
|
-
export * as Snapshot from "./Snapshot.ts";
|
|
19
|
-
export * as Stage from "./Stage.ts";
|
|
20
|
-
export * as Supervisor from "./Supervisor.ts";
|