fetchfox-sdk 1.0.2 → 1.0.3

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.3",
4
4
  "description": "AI scraper",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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/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,120 @@
1
+ import { jobs } from './jobs.js';
2
+
3
+ class FetchFoxError extends Error {};
4
+
5
+ const interval = 1_000;
6
+
7
+ export const Job = class {
8
+ #callbacks;
9
+
10
+ constructor(id) {
11
+ this.id = id;
12
+ this.#callbacks = {
13
+ completed: [],
14
+ error: [],
15
+ finished: [],
16
+ progress: [],
17
+ };
18
+
19
+ this.poll();
20
+ }
21
+
22
+ get _finished() {
23
+ return this._completed || this._error;
24
+ }
25
+
26
+ #select(data) {
27
+ return {
28
+ name: data.name,
29
+ state: data.state,
30
+ args: data.args,
31
+ metrics: data.metrics,
32
+ progress: data.progress,
33
+ results: data.results,
34
+ artifacts: data.artifacts,
35
+ };
36
+ }
37
+
38
+ async get() {
39
+ const data = await jobs.get(this.id);
40
+ const s = this.#select(data);
41
+ for (const key of Object.keys(s)) {
42
+ this[key] = s[key];
43
+ }
44
+ return this;
45
+ }
46
+
47
+ async poll() {
48
+ const last = JSON.stringify(this);
49
+ await this.get();
50
+ const didUpdate = JSON.stringify(this) != last;
51
+ if (didUpdate) {
52
+ console.log('Job progressed:', this);
53
+ this.trigger('progress');
54
+
55
+ if (this.state == 'completed') {
56
+ this._completed = true;
57
+ this.trigger('completed');
58
+ }
59
+ if (this.state == 'error') {
60
+ this._error = true;
61
+ this.trigger('error');
62
+ }
63
+
64
+ if (['completed', 'error'].includes(this.state)) {
65
+ this.trigger('finished');
66
+ }
67
+ }
68
+
69
+ if (!this._finished) {
70
+ setTimeout(() => this.poll(), interval);
71
+ }
72
+ }
73
+
74
+ checkEvent(event) {
75
+ if (!this.#callbacks[event]) {
76
+ throw new FetchFoxError(`Invalid event: ${event}`);
77
+ }
78
+ }
79
+
80
+ trigger(event) {
81
+ console.log('Trigger:', this.id, event);
82
+
83
+ this.checkEvent(event);
84
+ for (const cb of this.#callbacks[event]) {
85
+ cb({ ...this });
86
+ }
87
+ }
88
+
89
+ on(event, cb) {
90
+ this.checkEvent(event);
91
+ this.#callbacks[event].push(cb);
92
+ }
93
+
94
+ off(event, cb) {
95
+ this.checkEvent(event);
96
+ this.#callbacks[event] = this.#callbacks[event].filter(it => it != cb);
97
+ }
98
+
99
+ async waitFor(event) {
100
+ this.checkEvent(event);
101
+ return new Promise((ok) => {
102
+ this.on(event, () => {
103
+ this.off(event, ok);
104
+ ok({ ...this });
105
+ });
106
+ });
107
+ }
108
+
109
+ async completed() {
110
+ return this.waitFor('completed');
111
+ }
112
+
113
+ async error() {
114
+ return this.waitFor('error');
115
+ }
116
+
117
+ async finished() {
118
+ return this.waitFor('finished');
119
+ }
120
+ }
package/src/detach.js~ ADDED
File without changes
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
9
  export { configure } from './configure.js';