UNPKG

@oada/client

Version:

A lightweight client tool to interact with an OADA-compliant server

211 lines 7.25 kB
/** * @license * Copyright 2023 Open Ag Data Alliance * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import debug from "debug"; import { EventEmitter } from "eventemitter3"; import { JSONPath } from "jsonpath-plus"; import { deserializeError } from "serialize-error"; import { buildChangeObject, changeSym } from "./utils.js"; const log = { trace: debug("@oada/client/jobs:trace"), error: debug("@oada/client/jobs:error"), info: debug("@oada/client/jobs:info"), fatal: debug("@oada/client/jobs:fatal"), }; export class JobsRequest { job; oadaId; oadaListKey; oada; #emitter; // @ts-expect-error expect for now #watch; constructor({ oada, job }) { this.job = job; this.oada = oada; this.#emitter = new EventEmitter(); } async on(event, listener) { this.#emitter.on(event, this.#wrapListener(event, listener)); } async start() { const pending = `/bookmarks/services/${this.job.service}/jobs/pending`; const { headers } = await this.oada.post({ path: `/resources`, data: this.job, contentType: "application/vnd.oada.service.jobs.1+json", }); const _id = headers["content-location"]?.replace(/^\//, "") ?? ""; const key = _id.replace(/^resources\//, ""); // Const { _id, key } = await postJob(this.oada, pending, this.job as Json); this.oadaId = _id; this.oadaListKey = key; this.#watch = await this.#watchJob(); await this.oada.put({ path: `${pending}/${key}`, data: { _id, }, }); return { _id, key }; } /* async postUpdate(update: string | Json, status: string): Promise<void> { return postUpdate(this.oada, this.oadaId!, update, status || 'in-progress'); } */ #wrapListener(type, listener) { return async (jobChange) => { try { await listener(jobChange); } catch (error) { log.error({ type, listener: listener.name, error, }, "Error in job listener"); } }; } async #emit(event) { const getJob = this.#getJob.bind(this); let jobP; const out = { get job() { if (jobP === undefined) { jobP = getJob(); } return jobP; }, }; switch (event) { case JobEventType.Success: { this.#emitter.emit(JobEventType.Success, out); break; } case JobEventType.Status: { this.#emitter.emit(JobEventType.Status, out); break; } case JobEventType.Failure: { this.#emitter.emit(JobEventType.Failure, out); break; } case JobEventType.Result: { this.#emitter.emit(JobEventType.Result, out); break; } case JobEventType.Update: { this.#emitter.emit(JobEventType.Update, out); break; } default: { this.#emitter.emit(event, out); break; } } } async #watchJob() { const result = await this.oada.watch({ path: `/${this.oadaId}`, rev: 0, type: "tree", }); const { changes } = result; // eslint-disable-next-line github/no-then void this.#handleChangeFeed(changes).catch((error) => this.#emitter.emit("error", error)); log.info({ this: this }, "Job watch initialized"); return changes; } async #handleChangeFeed(watch) { for await (const [rootChange, ...children] of watch) { const changeBody = buildChangeObject(rootChange, ...children); await this.#handleJobChanges(changeBody); } log.fatal("Change feed ended unexpectedly"); throw new Error("Change feed ended"); } async #handleJobChanges(changeBody) { // eslint-disable-next-line new-cap const items = JSONPath({ resultType: "all", path: `$`, json: changeBody, }); for await (const { value } of items) { const { [changeSym]: changes } = value; for await (const change of changes ?? []) { log.trace({ change }, "Received change"); // TODO: determine whether we can get a resource at a particular rev // Emit generic item change event if (change?.body?.status === "success") await this.#emit(JobEventType.Success); if (change?.body?.status) await this.#emit(JobEventType.Status); if (change?.body?.status === "failure") await this.#emit(JobEventType.Failure); if (change.type === "merge" && change?.body?._rev >= 2 && change?.body?.result) await this.#emit(JobEventType.Result); if (change.type === "merge" && change?.body?.updates) await this.#emit(JobEventType.Update); } } } async #getJob() { // Needed because TS is weird about asserts... // const assertJob: TypeAssert<Job> = this.#assertJob; const { data } = await this.oada.get({ path: `/${this.oadaId}`, }); // AssertJob(item); return data; } } export var JobEventType; (function (JobEventType) { // The job is finished; a status arrives JobEventType["Status"] = "Status"; // Success status JobEventType["Success"] = "Success"; // Fail status JobEventType["Failure"] = "Failure"; // Result arrived JobEventType["Result"] = "Result"; // Update comes into the job JobEventType["Update"] = "Update"; /* // Job completed, any status result Done = 'Done', */ })(JobEventType || (JobEventType = {})); // TODO: should this be a JobConfig? export const doJob = async (oada, job) => new Promise((resolve, reject) => { const jr = new JobsRequest({ oada, job }); jr.on(JobEventType.Status, async ({ job: jo }) => { const index = await jo; if (index.status === "success") { resolve(index); } else if (index.status === "failure") { reject(deserializeError(index.result)); } }).catch(reject); jr.start(); }); //# sourceMappingURL=jobs.js.map