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.
Files changed (70) hide show
  1. package/README.md +94 -3
  2. package/dist/Browser-CiXHT1yO.d.mts +129 -0
  3. package/dist/{Browser-H63wiekr.mjs → Browser-CxXUjxOp.mjs} +10 -19
  4. package/dist/Browser-CxXUjxOp.mjs.map +1 -0
  5. package/dist/Browser.d.mts +2 -129
  6. package/dist/Browser.mjs +1 -1
  7. package/dist/Cdp-CzCRnMlM.d.mts +10 -0
  8. package/dist/Cdp.d.mts +5 -26
  9. package/dist/Cdp.mjs +4 -45
  10. package/dist/Cdp.mjs.map +1 -1
  11. package/dist/Chromium.d.mts +1 -1
  12. package/dist/Chromium.mjs +3 -3
  13. package/dist/Chromium.mjs.map +1 -1
  14. package/dist/Expect-u3P_ENLX.d.mts +62 -0
  15. package/dist/Expect.d.mts +2 -62
  16. package/dist/Expect.mjs +2 -2
  17. package/dist/LICENSE-jpeg-js +28 -0
  18. package/dist/Moment.mjs +1 -1
  19. package/dist/Page-BP02bmNv.mjs +264 -0
  20. package/dist/{Page-CfB6Zn1p.mjs.map → Page-BP02bmNv.mjs.map} +1 -1
  21. package/dist/Page.mjs +1 -1
  22. package/dist/{Plan-C-d5QYS8.mjs → Plan-BG1WifW2.mjs} +48 -4
  23. package/dist/Plan-BG1WifW2.mjs.map +1 -0
  24. package/dist/Plan.mjs +1 -1
  25. package/dist/Policy-BGWjbqCl.d.mts +76 -0
  26. package/dist/Policy.d.mts +2 -76
  27. package/dist/Policy.mjs +1 -1
  28. package/dist/Presentation-DxVikn9_.d.mts +48 -0
  29. package/dist/Presentation.d.mts +2 -48
  30. package/dist/Presentation.mjs +2 -2
  31. package/dist/Stage-NHp8N81k.d.mts +65 -0
  32. package/dist/Stage.d.mts +2 -65
  33. package/dist/Stage.mjs +2 -2
  34. package/dist/{Supervisor-mVPeEUp8.d.mts → Supervisor-CfRxbaso.d.mts} +2 -2
  35. package/dist/Supervisor.d.mts +1 -1
  36. package/dist/cdp-BMpYMWYe.mjs +69 -0
  37. package/dist/cdp-BMpYMWYe.mjs.map +1 -0
  38. package/dist/cdp-DL7XfEj5.d.mts +41 -0
  39. package/dist/{context-w3NJPRLo.mjs → context-EGhOqjNN.mjs} +5 -5
  40. package/dist/context-EGhOqjNN.mjs.map +1 -0
  41. package/dist/index.d.mts +7 -6
  42. package/dist/index.mjs +4 -3
  43. package/dist/page-PTM8m5wC.mjs +4057 -0
  44. package/dist/page-PTM8m5wC.mjs.map +1 -0
  45. package/dist/{url-Cd04Okid.mjs → url-Bsp2y55M.mjs} +2 -2
  46. package/dist/{url-Cd04Okid.mjs.map → url-Bsp2y55M.mjs.map} +1 -1
  47. package/dist/workerd/Cdp.d.mts +3 -0
  48. package/dist/workerd/Cdp.mjs +18 -0
  49. package/dist/workerd/Cdp.mjs.map +1 -0
  50. package/dist/workerd/index.d.mts +16 -0
  51. package/dist/workerd/index.mjs +17 -0
  52. package/package.json +15 -4
  53. package/src/Browser.ts +20 -30
  54. package/src/Cdp.ts +7 -92
  55. package/src/index.ts +1 -15
  56. package/src/internal/cdp.ts +134 -0
  57. package/src/internal/page/bridge.ts +2 -2
  58. package/src/internal/page/context.ts +21 -24
  59. package/src/internal/page/identity.ts +6 -25
  60. package/src/internal/pictures/motion.ts +3 -3
  61. package/src/workerd/Cdp.ts +11 -0
  62. package/src/workerd/index.ts +17 -0
  63. package/dist/Browser-H63wiekr.mjs.map +0 -1
  64. package/dist/Page-CfB6Zn1p.mjs +0 -610
  65. package/dist/Plan-C-d5QYS8.mjs.map +0 -1
  66. package/dist/context-w3NJPRLo.mjs.map +0 -1
  67. package/dist/match.inpage-xOe9LNO0.mjs +0 -48
  68. package/dist/match.inpage-xOe9LNO0.mjs.map +0 -1
  69. package/dist/page-BKwfF6z-.mjs +0 -5099
  70. 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 { redactWithin as a, FailFast as c, redact as i, make as l, parse as n, withheld as o, quoted as r, Correlation as s, addresses as t };
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-Cd04Okid.mjs.map
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,3 @@
1
+ import { n as Connector, r as Options, t as ConnectOptions } from "../cdp-DL7XfEj5.mjs";
2
+ import { n as layer, r as open } from "../Cdp-CzCRnMlM.mjs";
3
+ export { type ConnectOptions, type Connector, type Options, layer, open };
@@ -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.19",
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.0",
128
+ "@effect/vitest": "4.0.3",
119
129
  "@types/node": "26.1.2",
120
- "effect": "4.0.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.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 the Chrome DevTools Protocol: a Chrome started with
3
- * `--remote-debugging-port`, or any provider that hands out a CDP WebSocket URL.
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 * as Browser from "./Browser.ts";
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
- return new Failed({
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 = Effect.fn("Cdp.open")(function* (options: Options) {
59
- const endpoint = Redacted.isRedacted(options.endpoint)
60
- ? Redacted.value(options.endpoint)
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 * as Browser from "./Browser.ts";
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";