fetchfox-sdk 1.0.2 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fetchfox-sdk",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "AI scraper",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -28,6 +28,13 @@
28
28
  "eslint-plugin-promise": "^7.2.1",
29
29
  "husky": "^9.1.7",
30
30
  "jest": "^30.0.4",
31
+ "lint-staged": "^16.1.2",
31
32
  "prettier": "^3.6.2"
33
+ },
34
+ "lint-staged": {
35
+ "*.{js,jsx,ts,tsx,css,md}": "prettier --write"
36
+ },
37
+ "dependencies": {
38
+ "socket.io-client": "^4.8.1"
32
39
  }
33
40
  }
package/src/api.js CHANGED
@@ -1,31 +1,24 @@
1
1
  import { apiKey, host } from './configure.js';
2
2
 
3
- const endpoint = (path) => `${host()}${path}`;
4
-
5
- const clean = (dict) => {
6
- const result = {};
7
-
8
- for (const key in dict) {
9
- if (dict.hasOwnProperty(key)) {
10
- const snakeKey = key
11
- .replace(/([A-Z])/g, '_$1') // insert _ before uppercase letters
12
- .toLowerCase() // convert all to lowercase
13
- .replace(/^_/, ''); // remove leading _ if any
3
+ const camelCase = (obj) => {
4
+ const snakeToCamel = (str) =>
5
+ str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
6
+ return Object.fromEntries(
7
+ Object.entries(obj).map(([key, value]) => [snakeToCamel(key), value])
8
+ );
9
+ };
14
10
 
15
- result[snakeKey] = dict[key];
16
- }
17
- }
11
+ const endpoint = (path) => `${host()}${path}`;
18
12
 
19
- if (result.apiKey) {
20
- delete result.apiKey;
13
+ const FetchFoxAPIError = class extends Error {
14
+ constructor(errors) {
15
+ super(JSON.stringify(errors));
16
+ this.errors = errors;
21
17
  }
22
-
23
- return result;
24
18
  };
25
19
 
26
20
  export const call = async (method, path, params) => {
27
21
  const key = apiKey(params);
28
- params = clean(params);
29
22
 
30
23
  const args = {
31
24
  method,
@@ -43,5 +36,24 @@ export const call = async (method, path, params) => {
43
36
  }
44
37
 
45
38
  const resp = await fetch(url, args);
46
- return resp.json();
39
+ const text = await resp.text();
40
+ let data;
41
+ try {
42
+ data = JSON.parse(text);
43
+ } catch (e) {
44
+ throw new Error(`FetchFox returned invalid JSON: ${text}`);
45
+ }
46
+
47
+ if (data.errors) {
48
+ throw new FetchFoxAPIError(data.errors);
49
+ }
50
+
51
+ if (resp.status >= 400) {
52
+ throw new FetchFoxAPIError({
53
+ status: `Received status=${resp.status}`,
54
+ ...data,
55
+ });
56
+ }
57
+
58
+ return camelCase(data);
47
59
  };
package/src/configure.js CHANGED
@@ -2,11 +2,12 @@ const config = {
2
2
  host: 'https://api.fetchfox.ai',
3
3
  };
4
4
 
5
- const isNode = typeof process !== 'undefined' &&
6
- process.versions != null &&
7
- process.versions.node != null;
5
+ const isNode =
6
+ typeof process !== 'undefined' &&
7
+ process.versions != null &&
8
+ process.versions.node != null;
8
9
 
9
- const safeEnv = (key) => isNode ? process.env[key] : null;
10
+ const safeEnv = (key) => (isNode ? process.env[key] : null);
10
11
 
11
12
  export const configure = ({ apiKey, host }) => {
12
13
  if (apiKey) {
@@ -22,3 +23,9 @@ export const apiKey = (options) =>
22
23
 
23
24
  export const host = (options) =>
24
25
  options?.host || config.host || safeEnv('FETCHFOX_HOST');
26
+
27
+ export const ws = (options) =>
28
+ (options?.host || config.host || safeEnv('FETCHFOX_HOST')).replace(
29
+ 'http',
30
+ 'ws'
31
+ );
package/src/crawl.js CHANGED
@@ -1,5 +1,11 @@
1
1
  import { call } from './api.js';
2
+ import { Job } from './detach.js';
2
3
 
3
- export const crawl = async (args) => {
4
+ export const crawl = async function () {
4
5
  return call('POST', '/api/crawl', args);
5
6
  };
7
+ crawl.detach = async (args) => {
8
+ const data = await call('POST', '/api/crawl', { ...args, detach: true });
9
+ console.log('detach data', data);
10
+ return new Job(data.jobId);
11
+ };
package/src/detach.js ADDED
@@ -0,0 +1,139 @@
1
+ import { io } from 'socket.io-client';
2
+ import { ws } from './configure.js';
3
+ import { jobs } from './jobs.js';
4
+
5
+ class FetchFoxError extends Error {}
6
+
7
+ const interval = 1_000;
8
+
9
+ export function getSocket() {}
10
+
11
+ export const Job = class {
12
+ #callbacks;
13
+ #socket;
14
+
15
+ constructor(id) {
16
+ this.id = id;
17
+ this.#callbacks = {
18
+ completed: [],
19
+ error: [],
20
+ finished: [],
21
+ progress: [],
22
+ };
23
+
24
+ this.#socket = new io(ws());
25
+ this.#socket.on('progress', (data) => {
26
+ this.handleProgress(data);
27
+ console.log('==> socket got progress', data);
28
+ });
29
+ console.log('socket emit sub', this.id);
30
+ this.#socket.emit('sub', this.id);
31
+
32
+ // this.poll();
33
+ }
34
+
35
+ get _finished() {
36
+ return this._completed || this._error;
37
+ }
38
+
39
+ #select(data) {
40
+ const s = {};
41
+ for (const key of [
42
+ 'name',
43
+ 'state',
44
+ 'args',
45
+ 'metrics',
46
+ 'progress',
47
+ 'results',
48
+ 'artifacts',
49
+ 'timer',
50
+ ]) {
51
+ s[key] = data[key] || this[key];
52
+ }
53
+ return s;
54
+ }
55
+
56
+ async get() {
57
+ const data = await jobs.get(this.id);
58
+ const s = this.#select(data);
59
+ for (const key of Object.keys(s)) {
60
+ this[key] = s[key];
61
+ }
62
+ return this;
63
+ }
64
+
65
+ handleProgress(data) {
66
+ const last = JSON.stringify(this);
67
+
68
+ const s = this.#select(data);
69
+ for (const key of Object.keys(s)) {
70
+ this[key] = s[key];
71
+ }
72
+
73
+ const didUpdate = JSON.stringify(this) != last;
74
+ if (didUpdate) {
75
+ console.log('=> Job progressed:', this);
76
+ this.trigger('progress');
77
+
78
+ if (this.state == 'completed') {
79
+ this._completed = true;
80
+ this.trigger('completed');
81
+ }
82
+ if (this.state == 'error') {
83
+ this._error = true;
84
+ this.trigger('error');
85
+ }
86
+
87
+ if (['completed', 'error'].includes(this.state)) {
88
+ this.trigger('finished');
89
+ }
90
+ }
91
+ }
92
+
93
+ checkEvent(event) {
94
+ if (!this.#callbacks[event]) {
95
+ throw new FetchFoxError(`Invalid event: ${event}`);
96
+ }
97
+ }
98
+
99
+ trigger(event) {
100
+ console.log('Trigger:', this.id, event);
101
+
102
+ this.checkEvent(event);
103
+ for (const cb of this.#callbacks[event]) {
104
+ cb({ ...this });
105
+ }
106
+ }
107
+
108
+ on(event, cb) {
109
+ this.checkEvent(event);
110
+ this.#callbacks[event].push(cb);
111
+ }
112
+
113
+ off(event, cb) {
114
+ this.checkEvent(event);
115
+ this.#callbacks[event] = this.#callbacks[event].filter((it) => it != cb);
116
+ }
117
+
118
+ async waitFor(event) {
119
+ this.checkEvent(event);
120
+ return new Promise((ok) => {
121
+ this.on(event, () => {
122
+ this.off(event, ok);
123
+ ok({ ...this });
124
+ });
125
+ });
126
+ }
127
+
128
+ async completed() {
129
+ return this.waitFor('completed');
130
+ }
131
+
132
+ async error() {
133
+ return this.waitFor('error');
134
+ }
135
+
136
+ async finished() {
137
+ return this.waitFor('finished');
138
+ }
139
+ };
package/src/detach.js~ ADDED
File without changes
package/src/extract.js CHANGED
@@ -1,5 +1,12 @@
1
1
  import { call } from './api.js';
2
+ import { Job } from './detach.js';
2
3
 
3
4
  export const extract = async (args) => {
4
5
  return call('POST', '/api/extract', args);
5
6
  };
7
+
8
+ extract.detach = async (args) => {
9
+ const data = await call('POST', '/api/extract', { ...args, detach: true });
10
+ console.log('detach data', data);
11
+ return new Job(data.jobId);
12
+ };
package/src/index.js CHANGED
@@ -4,5 +4,6 @@ export * from './extract.js';
4
4
  export * from './jobs.js';
5
5
  export * from './user.js';
6
6
  export * from './proxy.js';
7
+ export { Job } from './detach.js';
7
8
  export { call } from './api.js';
8
- export { configure } from './configure.js';
9
+ export { configure, host, apiKey } from './configure.js';