omkit 0.4.7 → 0.4.8
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 +2 -1
- package/dist/actions/browser.d.ts +1 -1
- package/dist/actions/browser.js +1 -1
- package/dist/actions/chrome-page.d.ts +5 -4
- package/dist/actions/chrome-page.js +11 -11
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/port-free.d.ts +29 -0
- package/dist/actions/port-free.js +78 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ om("dev", async () => {
|
|
|
33
33
|
|
|
34
34
|
// …then launch a real browser and hand its live page downstream — typed, not scraped.
|
|
35
35
|
const chrome = browser({ headless: false }); // a visible Chromium, closed on teardown
|
|
36
|
-
const page = chromePage("app", chrome.ref, { url: "http://localhost:3000" });
|
|
36
|
+
const page = chromePage("app", await chrome.ref, { url: "http://localhost:3000" });
|
|
37
37
|
console.log(`opened ${(await page.ref).url()}`);
|
|
38
38
|
|
|
39
39
|
console.log("stack is up — press Ctrl+C to stop");
|
|
@@ -223,6 +223,7 @@ Reusable actions built on the core engine. For anything beyond these, drop down
|
|
|
223
223
|
- **`untilLog`** — gate on the first log entry matching a predicate.
|
|
224
224
|
- **`tailLog`** — tail a file another process writes, folding its lines into the combined log.
|
|
225
225
|
- **`healthcheck`** — poll a URL/port until the status (and optionally body) matches.
|
|
226
|
+
- **`portFree`** — the mirror of `healthcheck`: poll a TCP port until nothing is listening, so a restart can rebind without racing the old process.
|
|
226
227
|
- **`prompt`** — ask the terminal for input or a choice, with a timeout that falls back to a default; the answer is its handle.
|
|
227
228
|
- **`browser`** — launch a Chromium browser with Playwright and expose the live `Browser` as a handle (defaults to the installed Chrome); closed on teardown. Feed its `.ref` to `chromePage`.
|
|
228
229
|
- **`chromePage`** — attach to Chrome over CDP, or to an existing Playwright `Page`/`Browser`/`Context` (including a `browser` handle or an Electron window), and expose the live `Page` as a handle.
|
|
@@ -17,7 +17,7 @@ export interface BrowserOptions {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Launches a Chromium browser with Playwright and publishes the live {@link Browser}
|
|
20
|
-
* as this action's handle, so a driver — e.g. `chromePage("Explorer", browser.ref,
|
|
20
|
+
* as this action's handle, so a driver — e.g. `chromePage("Explorer", await browser.ref,
|
|
21
21
|
* { url })` — can await `instance.ref` and open pages in it. This owns the browser it
|
|
22
22
|
* launches (unlike a CDP endpoint handed to `chromePage`, which belongs to its opener),
|
|
23
23
|
* so teardown closes it. Replaces the external chromedriver/WebDriver dance.
|
package/dist/actions/browser.js
CHANGED
|
@@ -3,7 +3,7 @@ import { action } from "../core/action.js";
|
|
|
3
3
|
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
4
|
/**
|
|
5
5
|
* Launches a Chromium browser with Playwright and publishes the live {@link Browser}
|
|
6
|
-
* as this action's handle, so a driver — e.g. `chromePage("Explorer", browser.ref,
|
|
6
|
+
* as this action's handle, so a driver — e.g. `chromePage("Explorer", await browser.ref,
|
|
7
7
|
* { url })` — can await `instance.ref` and open pages in it. This owns the browser it
|
|
8
8
|
* launches (unlike a CDP endpoint handed to `chromePage`, which belongs to its opener),
|
|
9
9
|
* so teardown closes it. Replaces the external chromedriver/WebDriver dance.
|
|
@@ -22,10 +22,11 @@ export interface ChromePageOptions {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Publishes a Chrome/Chromium {@link Page} as this action's handle, so downstream
|
|
25
|
-
* actions can await `instance.ref` and drive the same live page. The source
|
|
26
|
-
* a promise
|
|
27
|
-
*
|
|
25
|
+
* actions can await `instance.ref` and drive the same live page. The source is a
|
|
26
|
+
* resolved handle, not a promise — chain it from another action by awaiting that
|
|
27
|
+
* action's `.ref` at the call site (e.g.
|
|
28
|
+
* `chromePage("Explorer", await chromedriver.ref)`). `label` tags this run so several
|
|
28
29
|
* pages in one run stay distinguishable. Daemon: acquire + attach, stay alive
|
|
29
30
|
* until teardown, then close the CDP connection only when it opened one.
|
|
30
31
|
*/
|
|
31
|
-
export declare const chromePage: import("../index.ts").Action<[label: string, source: ChromePageSource
|
|
32
|
+
export declare const chromePage: import("../index.ts").Action<[label: string, source: ChromePageSource, opts?: ChromePageOptions | undefined], void, ChromePageEvents, Page>;
|
|
@@ -7,9 +7,10 @@ const isBrowser = (x) => typeof x.contexts === "function";
|
|
|
7
7
|
const pageOf = (context, fresh) => fresh ? context.newPage() : (context.pages()[0] ?? context.newPage());
|
|
8
8
|
/**
|
|
9
9
|
* Publishes a Chrome/Chromium {@link Page} as this action's handle, so downstream
|
|
10
|
-
* actions can await `instance.ref` and drive the same live page. The source
|
|
11
|
-
* a promise
|
|
12
|
-
*
|
|
10
|
+
* actions can await `instance.ref` and drive the same live page. The source is a
|
|
11
|
+
* resolved handle, not a promise — chain it from another action by awaiting that
|
|
12
|
+
* action's `.ref` at the call site (e.g.
|
|
13
|
+
* `chromePage("Explorer", await chromedriver.ref)`). `label` tags this run so several
|
|
13
14
|
* pages in one run stay distinguishable. Daemon: acquire + attach, stay alive
|
|
14
15
|
* until teardown, then close the CDP connection only when it opened one.
|
|
15
16
|
*/
|
|
@@ -19,13 +20,12 @@ export const chromePage = action("chromePage")
|
|
|
19
20
|
.run(async ({ signal, emit, attach, tag }, label, source, opts = {}) => {
|
|
20
21
|
tag(label);
|
|
21
22
|
const { url } = opts;
|
|
22
|
-
const resolved = await source;
|
|
23
23
|
// Only a CDP connection we open here is ours to close; handles belong to callers.
|
|
24
24
|
let owned;
|
|
25
25
|
let page;
|
|
26
26
|
let from;
|
|
27
|
-
if (typeof
|
|
28
|
-
const cdpUrl =
|
|
27
|
+
if (typeof source === "string") {
|
|
28
|
+
const cdpUrl = source.includes("://") ? source : `http://${source}`;
|
|
29
29
|
from = cdpUrl;
|
|
30
30
|
try {
|
|
31
31
|
owned = await chromium.connectOverCDP(cdpUrl);
|
|
@@ -36,18 +36,18 @@ export const chromePage = action("chromePage")
|
|
|
36
36
|
const context = owned.contexts()[0] ?? (await owned.newContext());
|
|
37
37
|
page = await pageOf(context, url !== undefined);
|
|
38
38
|
}
|
|
39
|
-
else if (isPage(
|
|
39
|
+
else if (isPage(source)) {
|
|
40
40
|
from = "page handle";
|
|
41
|
-
page =
|
|
41
|
+
page = source;
|
|
42
42
|
}
|
|
43
|
-
else if (isBrowser(
|
|
43
|
+
else if (isBrowser(source)) {
|
|
44
44
|
from = "browser handle";
|
|
45
|
-
const context =
|
|
45
|
+
const context = source.contexts()[0] ?? (await source.newContext());
|
|
46
46
|
page = await pageOf(context, url !== undefined);
|
|
47
47
|
}
|
|
48
48
|
else {
|
|
49
49
|
from = "context handle";
|
|
50
|
-
page = await pageOf(
|
|
50
|
+
page = await pageOf(source, url !== undefined);
|
|
51
51
|
}
|
|
52
52
|
page.on("console", (msg) => {
|
|
53
53
|
console.log(`console: ${msg.text()}`);
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { untilLog } from "./until-log.ts";
|
|
|
8
8
|
export type { LogMatcher, UntilLogOptions, UntilLogEvents } from "./until-log.ts";
|
|
9
9
|
export { healthcheck } from "./healthcheck.ts";
|
|
10
10
|
export type { HealthcheckOptions, HealthcheckResult, HealthcheckEvents, StatusMatcher, BodyMatcher, } from "./healthcheck.ts";
|
|
11
|
+
export { portFree } from "./port-free.ts";
|
|
12
|
+
export type { PortFreeOptions, PortFreeResult, PortFreeEvents } from "./port-free.ts";
|
|
11
13
|
export { watch } from "./watch.ts";
|
|
12
14
|
export type { FileEvent, FileListener, WatchOptions } from "./watch.ts";
|
|
13
15
|
export { watchDir } from "./watch-dir.ts";
|
package/dist/actions/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export { command } from "./command.js";
|
|
6
6
|
export { untilLog } from "./until-log.js";
|
|
7
7
|
export { healthcheck } from "./healthcheck.js";
|
|
8
|
+
export { portFree } from "./port-free.js";
|
|
8
9
|
export { watch } from "./watch.js";
|
|
9
10
|
export { watchDir } from "./watch-dir.js";
|
|
10
11
|
export { tailLog } from "./tail-log.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface PortFreeOptions {
|
|
2
|
+
/** Host to probe. Default "127.0.0.1". */
|
|
3
|
+
host?: string;
|
|
4
|
+
/** Delay between probes. Default 500ms. */
|
|
5
|
+
intervalMs?: number;
|
|
6
|
+
/** Abort a single connect attempt that hasn't answered within this long. Default 1000ms. */
|
|
7
|
+
connectTimeoutMs?: number;
|
|
8
|
+
/** Reject after this long overall. Default 30000ms; pass `undefined` to wait until teardown. */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/** The probe that found the port free — this action's result and `free` event payload. */
|
|
12
|
+
export interface PortFreeResult {
|
|
13
|
+
port: number;
|
|
14
|
+
host: string;
|
|
15
|
+
attempts: number;
|
|
16
|
+
}
|
|
17
|
+
/** Events emitted by {@link portFree}. */
|
|
18
|
+
export interface PortFreeEvents {
|
|
19
|
+
/** Nothing is listening any more; payload is the passing probe. */
|
|
20
|
+
free: PortFreeResult;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Gate: polls a TCP port until nothing is listening on it, then resolves with that
|
|
24
|
+
* probe and emits `free`. The mirror of `healthcheck` — where that one waits
|
|
25
|
+
* for a service to come up, this waits for one to let go, so a restart can rebind
|
|
26
|
+
* without racing the previous process's socket. Bounded by the run's abort signal
|
|
27
|
+
* and `timeoutMs`, so teardown unblocks a pending probe instead of hanging.
|
|
28
|
+
*/
|
|
29
|
+
export declare const portFree: import("../index.ts").Action<[port: number, opts?: PortFreeOptions | undefined], PortFreeResult, PortFreeEvents, void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { action } from "../core/action.js";
|
|
3
|
+
/**
|
|
4
|
+
* One TCP connect attempt. A refusal is the only answer that proves nobody is
|
|
5
|
+
* listening: a successful connect means the port is taken, and a hang or any other
|
|
6
|
+
* error (filtered, unreachable) is inconclusive, so both count as still busy rather
|
|
7
|
+
* than letting a firewall read as "free".
|
|
8
|
+
*/
|
|
9
|
+
const probe = (port, host, connectTimeoutMs, signal) => new Promise((resolve) => {
|
|
10
|
+
const socket = new net.Socket();
|
|
11
|
+
let settled = false;
|
|
12
|
+
const finish = (free) => {
|
|
13
|
+
if (settled)
|
|
14
|
+
return;
|
|
15
|
+
settled = true;
|
|
16
|
+
signal.removeEventListener("abort", onAbort);
|
|
17
|
+
socket.destroy();
|
|
18
|
+
resolve(free);
|
|
19
|
+
};
|
|
20
|
+
// Teardown mid-connect: drop the socket and report "busy" — the loop's own abort
|
|
21
|
+
// check turns that into a clean stop on the next pass.
|
|
22
|
+
function onAbort() {
|
|
23
|
+
finish(false);
|
|
24
|
+
}
|
|
25
|
+
socket.setTimeout(connectTimeoutMs);
|
|
26
|
+
socket.once("connect", () => finish(false));
|
|
27
|
+
socket.once("timeout", () => finish(false));
|
|
28
|
+
socket.once("error", (error) => finish(error.code === "ECONNREFUSED"));
|
|
29
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
30
|
+
socket.connect(port, host);
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Gate: polls a TCP port until nothing is listening on it, then resolves with that
|
|
34
|
+
* probe and emits `free`. The mirror of `healthcheck` — where that one waits
|
|
35
|
+
* for a service to come up, this waits for one to let go, so a restart can rebind
|
|
36
|
+
* without racing the previous process's socket. Bounded by the run's abort signal
|
|
37
|
+
* and `timeoutMs`, so teardown unblocks a pending probe instead of hanging.
|
|
38
|
+
*/
|
|
39
|
+
export const portFree = action("portFree")
|
|
40
|
+
.emits()
|
|
41
|
+
.run(async ({ signal, emit }, port, opts = {}) => {
|
|
42
|
+
const { host = "127.0.0.1", intervalMs = 500, connectTimeoutMs = 1000, timeoutMs = 30_000, } = opts;
|
|
43
|
+
// Abortable inter-probe delay: resolves after `ms`, or rejects on teardown.
|
|
44
|
+
const sleep = (ms) => new Promise((resolve, reject) => {
|
|
45
|
+
const onAbort = () => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
reject(new Error("portFree: aborted"));
|
|
48
|
+
};
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
signal.removeEventListener("abort", onAbort);
|
|
51
|
+
resolve();
|
|
52
|
+
}, ms);
|
|
53
|
+
if (signal.aborted)
|
|
54
|
+
onAbort();
|
|
55
|
+
else
|
|
56
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
57
|
+
});
|
|
58
|
+
const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
59
|
+
const target = `${host}:${port}`;
|
|
60
|
+
console.log(`waiting for ${target} to free up`);
|
|
61
|
+
let attempts = 0;
|
|
62
|
+
for (;;) {
|
|
63
|
+
if (signal.aborted)
|
|
64
|
+
throw new Error("portFree: aborted");
|
|
65
|
+
attempts++;
|
|
66
|
+
if (await probe(port, host, connectTimeoutMs, signal)) {
|
|
67
|
+
const result = { port, host, attempts };
|
|
68
|
+
const probes = `${attempts} ${attempts === 1 ? "probe" : "probes"}`;
|
|
69
|
+
console.log(`free ${target} (${probes})`);
|
|
70
|
+
emit("free", result);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
if (deadline !== undefined && Date.now() >= deadline) {
|
|
74
|
+
throw new Error(`portFree: ${target} still in use after ${timeoutMs}ms (${attempts} probes)`);
|
|
75
|
+
}
|
|
76
|
+
await sleep(intervalMs);
|
|
77
|
+
}
|
|
78
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|