@opencraw/driver 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@ npx opencraw-driver # then write JSON-RPC lines to it
47
47
  | `work.done`, `work.failed` | driver | How an item ended: its report and records, or its outcome (`failure`, `neutral`). |
48
48
  | `callout` | driver | A hook, captcha solver or access plugin the client serves (`request.kind`: `hook`, `captcha`, `access`). The request and its answer are core's [callout contract](../core/README.md): `ok`, `error`, or `pending` with `retryAfterMs`, asked again with the same idempotency key. |
49
49
 
50
- Every answer comes back before the driver stops, even when a client writes its last requests and closes
50
+ A run's answer (`run`, `work`) always comes before any of its events, records or callouts. Every answer comes back before the driver stops, even when a client writes its last requests and closes
51
51
  stdin at once. Answers may arrive out of order; match them by `id`.
52
52
 
53
53
  ## For client authors
package/dist/index.esm.js CHANGED
@@ -240,8 +240,8 @@ function clientAccessPlugins(names, runId, ask) {
240
240
  // src/site-access/resolve-site-access.use-case.ts
241
241
  import { resolveAccess } from "@opencraw/cli";
242
242
  async function resolveSiteAccess(params, runId, ask, env = process.env) {
243
- const file = params.accessConfig ?? env.OPENCRAW_ACCESS;
244
- if (file === void 0 && params.access !== void 0) throw new Error(`access profile "${params.access}" needs an access config: give "accessConfig", or set OPENCRAW_ACCESS where the driver runs`);
243
+ const file = params.accessConfig ?? (env.OPENCRAW_ACCESS === "" ? void 0 : env.OPENCRAW_ACCESS);
244
+ if (file === void 0 && params.access !== void 0) throw new Error(`access profile "${params.access}" needs an access config, and there is none: no "accessConfig" was given, and OPENCRAW_ACCESS is not set where OpenCraw runs`);
245
245
  const access = await resolveAccess({ insecureTls: false, access: file, accessProfile: params.access });
246
246
  return { access, accessPlugins: clientAccessPlugins(params.accessPlugins ?? [], runId, ask) };
247
247
  }
@@ -344,17 +344,17 @@ function streamingSink() {
344
344
  }
345
345
 
346
346
  // src/session/run-session.use-case.ts
347
- function runSessions(notify, ask) {
347
+ function runSessions(notify, ask, local = {}) {
348
348
  const running = /* @__PURE__ */ new Map();
349
349
  let next = 1;
350
350
  const crawlerFor = async (runId, params) => {
351
351
  const { access, accessPlugins } = await resolveSiteAccess(params, runId, ask);
352
352
  return createCrawler({
353
353
  sink: params.out === void 0 ? streamingSink() : fileSink2(params.out, { append: params.append, browser: { executablePath: params.browser?.executablePath } }),
354
- hooks: clientHooks(params.hooks ?? [], runId, ask),
355
- captchaSolvers: clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask),
354
+ hooks: { ...local.hooks, ...clientHooks(params.hooks ?? [], runId, ask) },
355
+ captchaSolvers: [...local.captchaSolvers ?? [], ...clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask)],
356
356
  access,
357
- accessPlugins,
357
+ accessPlugins: [...local.accessPlugins ?? [], ...accessPlugins],
358
358
  profilesDir: process.env.OPENCRAW_PROFILES,
359
359
  parallel: params.parallel,
360
360
  resume: params.resume,
@@ -369,6 +369,7 @@ function runSessions(notify, ask) {
369
369
  const track = (runId, crawler, crawling) => {
370
370
  running.set(runId, crawler);
371
371
  void (async () => {
372
+ await new Promise((resolve) => setImmediate(resolve));
372
373
  try {
373
374
  notify("finished", { runId, report: await crawling() });
374
375
  } catch (error) {
@@ -379,15 +380,16 @@ function runSessions(notify, ask) {
379
380
  }
380
381
  })();
381
382
  };
383
+ const startLoaded = async (loaded, params) => {
384
+ const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
385
+ const runId = `run-${next++}`;
386
+ const crawler = await crawlerFor(runId, params);
387
+ track(runId, crawler, async () => await crawler.run(set));
388
+ return runId;
389
+ };
382
390
  return {
383
- start: async (params) => {
384
- const loaded = await loadRecipes(params);
385
- const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
386
- const runId = `run-${next++}`;
387
- const crawler = await crawlerFor(runId, params);
388
- track(runId, crawler, async () => await crawler.run(set));
389
- return runId;
390
- },
391
+ start: async (params) => await startLoaded(await loadRecipes(params), params),
392
+ startLoaded,
391
393
  startWork: async (params) => {
392
394
  const set = await loadRecipes(params);
393
395
  const runId = `run-${next++}`;
package/dist/main.esm.js CHANGED
@@ -201,8 +201,8 @@ function clientAccessPlugins(names, runId, ask) {
201
201
  // src/site-access/resolve-site-access.use-case.ts
202
202
  import { resolveAccess } from "@opencraw/cli";
203
203
  async function resolveSiteAccess(params, runId, ask, env = process.env) {
204
- const file = params.accessConfig ?? env.OPENCRAW_ACCESS;
205
- if (file === void 0 && params.access !== void 0) throw new Error(`access profile "${params.access}" needs an access config: give "accessConfig", or set OPENCRAW_ACCESS where the driver runs`);
204
+ const file = params.accessConfig ?? (env.OPENCRAW_ACCESS === "" ? void 0 : env.OPENCRAW_ACCESS);
205
+ if (file === void 0 && params.access !== void 0) throw new Error(`access profile "${params.access}" needs an access config, and there is none: no "accessConfig" was given, and OPENCRAW_ACCESS is not set where OpenCraw runs`);
206
206
  const access = await resolveAccess({ insecureTls: false, access: file, accessProfile: params.access });
207
207
  return { access, accessPlugins: clientAccessPlugins(params.accessPlugins ?? [], runId, ask) };
208
208
  }
@@ -305,17 +305,17 @@ function streamingSink() {
305
305
  }
306
306
 
307
307
  // src/session/run-session.use-case.ts
308
- function runSessions(notify, ask) {
308
+ function runSessions(notify, ask, local = {}) {
309
309
  const running = /* @__PURE__ */ new Map();
310
310
  let next = 1;
311
311
  const crawlerFor = async (runId, params) => {
312
312
  const { access, accessPlugins } = await resolveSiteAccess(params, runId, ask);
313
313
  return createCrawler({
314
314
  sink: params.out === void 0 ? streamingSink() : fileSink2(params.out, { append: params.append, browser: { executablePath: params.browser?.executablePath } }),
315
- hooks: clientHooks(params.hooks ?? [], runId, ask),
316
- captchaSolvers: clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask),
315
+ hooks: { ...local.hooks, ...clientHooks(params.hooks ?? [], runId, ask) },
316
+ captchaSolvers: [...local.captchaSolvers ?? [], ...clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask)],
317
317
  access,
318
- accessPlugins,
318
+ accessPlugins: [...local.accessPlugins ?? [], ...accessPlugins],
319
319
  profilesDir: process.env.OPENCRAW_PROFILES,
320
320
  parallel: params.parallel,
321
321
  resume: params.resume,
@@ -330,6 +330,7 @@ function runSessions(notify, ask) {
330
330
  const track = (runId, crawler, crawling) => {
331
331
  running.set(runId, crawler);
332
332
  void (async () => {
333
+ await new Promise((resolve) => setImmediate(resolve));
333
334
  try {
334
335
  notify("finished", { runId, report: await crawling() });
335
336
  } catch (error) {
@@ -340,15 +341,16 @@ function runSessions(notify, ask) {
340
341
  }
341
342
  })();
342
343
  };
344
+ const startLoaded = async (loaded, params) => {
345
+ const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
346
+ const runId = `run-${next++}`;
347
+ const crawler = await crawlerFor(runId, params);
348
+ track(runId, crawler, async () => await crawler.run(set));
349
+ return runId;
350
+ };
343
351
  return {
344
- start: async (params) => {
345
- const loaded = await loadRecipes(params);
346
- const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
347
- const runId = `run-${next++}`;
348
- const crawler = await crawlerFor(runId, params);
349
- track(runId, crawler, async () => await crawler.run(set));
350
- return runId;
351
- },
352
+ start: async (params) => await startLoaded(await loadRecipes(params), params),
353
+ startLoaded,
352
354
  startWork: async (params) => {
353
355
  const set = await loadRecipes(params);
354
356
  const runId = `run-${next++}`;
@@ -5,7 +5,7 @@ export type { InitializeParams, InitializeResult, ValidateParams, ValidateResult
5
5
  export { openLineChannel, RPC_ERRORS, RpcFailure } from './transport/index.js';
6
6
  export type { LineChannel, RequestHandler, RpcError, RpcMessage } from './transport/index.js';
7
7
  export { runSessions, validateRecipes, streamingSink } from './session/index.js';
8
- export type { RunSessions } from './session/index.js';
8
+ export type { LocalPlugins, RunSessions } from './session/index.js';
9
9
  export { clientHooks, clientCaptchaSolvers, clientAccessPlugins } from './callout-bridge/index.js';
10
10
  export type { AskClient, NotifyClient } from './callout-bridge/index.js';
11
11
  export { clientWorkSource } from './work-bridge/index.js';
@@ -1,5 +1,5 @@
1
1
  export { runSessions } from './run-session.use-case.js';
2
- export type { RunSessions } from './run-session.use-case.js';
2
+ export type { LocalPlugins, RunSessions } from './run-session.use-case.js';
3
3
  export { validateRecipes, hooksOf, loadRecipes, sourceOf } from './recipe-loading.use-case.js';
4
4
  export { streamingSink } from './streaming-sink.repository.js';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,12 +1,22 @@
1
+ import { RecipeSet } from '@opencraw/core';
2
+ import type { AccessPlugin, CaptchaSolver, HookMap } from '@opencraw/core';
1
3
  import type { AskClient, NotifyClient } from '../callout-bridge/index.js';
2
4
  import type { RunParams, WorkParams } from '../protocol/index.js';
3
5
  /** The runs a driver has going, by id, so `cancel` can reach them and `shutdown` close them all. */
4
6
  export interface RunSessions {
5
7
  start: (params: RunParams) => Promise<string>;
8
+ /** As `start`, for recipes the host has already loaded and bound (`params`' recipe source is ignored). */
9
+ startLoaded: (set: RecipeSet, params: Omit<RunParams, 'paths' | 'recipes'>) => Promise<string>;
6
10
  startWork: (params: WorkParams) => Promise<string>;
7
11
  cancel: (runId: string) => Promise<boolean>;
8
12
  closeAll: () => Promise<void>;
9
13
  }
14
+ /** What the host serves itself, in process, besides what the client serves: an MCP server's plugins module. */
15
+ export interface LocalPlugins {
16
+ hooks?: HookMap;
17
+ captchaSolvers?: CaptchaSolver[];
18
+ accessPlugins?: AccessPlugin[];
19
+ }
10
20
  /**
11
21
  * Starts and tracks runs and worker pools. `start` and `startWork` load and bind the recipes (a bad
12
22
  * recipe fails the request itself, with its problems), answer with a run id at once, and crawl in the
@@ -18,7 +28,8 @@ export interface RunSessions {
18
28
  *
19
29
  * @param notify - The channel's notify.
20
30
  * @param ask - The channel's request, for callouts and work.
31
+ * @param local - Hooks, captcha solvers and access plugins the host serves itself, beside the client's.
21
32
  * @returns The sessions.
22
33
  */
23
- export declare function runSessions(notify: NotifyClient, ask: AskClient): RunSessions;
34
+ export declare function runSessions(notify: NotifyClient, ask: AskClient, local?: LocalPlugins): RunSessions;
24
35
  //# sourceMappingURL=run-session.use-case.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencraw/driver",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "OpenCraw's driver: the engine over JSON-RPC on stdio, for clients in other languages",
5
5
  "license": "MIT",
6
6
  "type": "module",