@cycleplatform/api-client-typescript 0.2.5 → 0.3.4

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/src/index.ts CHANGED
@@ -3,6 +3,8 @@ import type { paths, components, operations } from "./generated/types";
3
3
 
4
4
  export { paths, components, operations };
5
5
 
6
+ export { trackJob, getJobProgress } from "./jobs";
7
+
6
8
  export function getClient({
7
9
  apiKey,
8
10
  baseUrl = "https://api.cycle.io",
@@ -20,10 +22,10 @@ export function getClient({
20
22
  });
21
23
 
22
24
  const authMiddleware: Middleware = {
23
- async onRequest(req) {
24
- req.headers.set("Authorization", `Bearer ${apiKey}`);
25
- req.headers.set("X-Hub-Id", hubId);
26
- return req;
25
+ async onRequest({ request }) {
26
+ request.headers.set("Authorization", `Bearer ${apiKey}`);
27
+ request.headers.set("X-Hub-Id", hubId);
28
+ return request;
27
29
  },
28
30
  };
29
31
 
package/src/jobs.ts ADDED
@@ -0,0 +1,167 @@
1
+ import { getClient } from ".";
2
+ import { type components } from "./generated/types";
3
+
4
+ type TrackJobSettings = {
5
+ pollingInterval?: number;
6
+ };
7
+
8
+ type JobProgressEvent = ReturnType<typeof getJobProgress> & {
9
+ state: components["schemas"]["JobState"]["current"];
10
+ };
11
+
12
+ class JobTracker extends EventTarget {
13
+ promise: Promise<components["schemas"]["Job"]>;
14
+
15
+ constructor(
16
+ client: ReturnType<typeof getClient>,
17
+ job: string | { id: string },
18
+ customSettings?: TrackJobSettings
19
+ ) {
20
+ super();
21
+ this.promise = this.track(client, job, customSettings);
22
+ }
23
+
24
+ private async track(
25
+ client: ReturnType<typeof getClient>,
26
+ job: string | { id: string },
27
+ customSettings?: TrackJobSettings
28
+ ): Promise<components["schemas"]["Job"]> {
29
+ const jobId = typeof job === "object" ? job.id : job;
30
+ const settings: TrackJobSettings = {
31
+ pollingInterval: 2000,
32
+ ...customSettings,
33
+ };
34
+
35
+ let retryCounter = 0;
36
+
37
+ while (true) {
38
+ const jresp = await client.GET("/v1/jobs/{jobId}", {
39
+ params: { path: { jobId } },
40
+ });
41
+
42
+ if (jresp.error) {
43
+ if (retryCounter < 5 && jresp.error.error.code === "404.job") {
44
+ retryCounter++;
45
+ await this.delay(settings.pollingInterval);
46
+ continue;
47
+ }
48
+ throw jresp.error;
49
+ }
50
+
51
+ const jobData = jresp.data.data;
52
+
53
+ const progress = getJobProgress(jobData);
54
+
55
+ this.dispatchEvent(
56
+ new CustomEvent<JobProgressEvent>("progress", {
57
+ detail: { ...progress, state: jobData.state.current },
58
+ })
59
+ );
60
+
61
+ if (isJobDone(jobData)) {
62
+ if (jobData.state.current === "error") {
63
+ new CustomEvent<components["schemas"]["Job"]>("error", {
64
+ detail: jobData,
65
+ });
66
+ throw jobData;
67
+ }
68
+
69
+ this.dispatchEvent(
70
+ new CustomEvent<components["schemas"]["Job"]>("done", {
71
+ detail: jobData,
72
+ })
73
+ );
74
+ return jobData;
75
+ }
76
+
77
+ await this.delay(settings.pollingInterval);
78
+ }
79
+ }
80
+
81
+ private delay(ms: number = 2000) {
82
+ return new Promise((res) => setTimeout(res, ms));
83
+ }
84
+ }
85
+
86
+ function isJobDone(job: components["schemas"]["Job"]) {
87
+ const doneState = [
88
+ "completed",
89
+ "error",
90
+ "expired",
91
+ ] as components["schemas"]["JobState"]["current"][];
92
+ return doneState.includes(job.state.current);
93
+ }
94
+
95
+ export const zeroTimeString = "0001-01-01T00:00:00Z";
96
+ /**
97
+ * Gets a breakdown of a job's progress
98
+ * @param job A Cycle job
99
+ * @returns an analysis of job progress
100
+ */
101
+ export function getJobProgress(job: components["schemas"]["Job"]) {
102
+ if (!job.tasks || job.tasks.length === 0) {
103
+ return { total: 0, completed: 0, failed: 0, percent: 100 };
104
+ }
105
+
106
+ const stepAnalysis = job.tasks.reduce(
107
+ (acc, cur) => {
108
+ const taskStepCount = cur.steps?.length || 1;
109
+ acc["total"] += taskStepCount;
110
+
111
+ // task hasn't started yet
112
+ if (cur.state.current === "pending") {
113
+ return acc;
114
+ }
115
+
116
+ // no steps
117
+ if (!cur.steps?.length) {
118
+ switch (cur.state.current) {
119
+ case "completed":
120
+ acc["completed"] += 1;
121
+ break;
122
+ case "error":
123
+ acc["failed"] += 1;
124
+ }
125
+ return acc;
126
+ }
127
+
128
+ cur.steps?.forEach((s) => {
129
+ if (s.completed !== zeroTimeString) {
130
+ acc["completed"] += 1;
131
+ return;
132
+ }
133
+
134
+ if (
135
+ ["error", "completed"].includes(cur.state.current || "") &&
136
+ s.started !== zeroTimeString
137
+ ) {
138
+ // task is done but step didn't complete
139
+ acc["failed"] += 1;
140
+ }
141
+ });
142
+
143
+ return acc;
144
+ },
145
+ { total: 0, completed: 0, failed: 0 }
146
+ );
147
+
148
+ return {
149
+ ...stepAnalysis,
150
+ percent: (stepAnalysis.completed / stepAnalysis.total) * 100,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Tracks a job in Cycle API and allows checking its progress.
156
+ * @param client The Cycle API client instance
157
+ * @param job A job ID or object containing an id field containing the ID of the job
158
+ * @param customSettings Settings like polling interval in ms
159
+ * @returns An object with a Promise and an EventTarget for tracking progress
160
+ */
161
+ export function trackJob(
162
+ client: ReturnType<typeof getClient>,
163
+ job: string | { id: string },
164
+ customSettings?: TrackJobSettings
165
+ ) {
166
+ return new JobTracker(client, job, customSettings);
167
+ }
package/tsconfig.json CHANGED
@@ -15,12 +15,14 @@
15
15
  "moduleResolution": "node",
16
16
  "noUnusedLocals": true,
17
17
  "noUnusedParameters": true,
18
+ "noUncheckedIndexedAccess": true,
18
19
  "preserveWatchOutput": true,
19
20
  "skipLibCheck": true,
20
21
  "strict": true
21
22
  },
22
- "include": ["./src"],
23
+ "include": ["src", "tests"],
23
24
  "exclude": [
24
- "node_modules"
25
+ "node_modules",
26
+ "dist"
25
27
  ]
26
28
  }