Skip to content

Commit ed0340b

Browse files
committed
Implement x-arango-async: store
Fixes DE-610.
1 parent b7c4f23 commit ed0340b

File tree

3 files changed

+137
-12
lines changed

3 files changed

+137
-12
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ This driver uses semantic versioning:
1414
- A change in the major version (e.g. 1.Y.Z -> 2.0.0) indicates _breaking_
1515
changes that require changes in your code to upgrade.
1616

17+
## [Unreleased]
18+
19+
### Added
20+
21+
- Added `db.createJob` method to convert arbitrary requests into async jobs (DE-610)
22+
23+
This method can be used to set the `x-arango-async: store` header on any
24+
request, which will cause the server to store the request in an async job:
25+
26+
```js
27+
const collectionsJob = await db.createJob(() => db.collections());
28+
// once loaded, collectionsJob.result will be an array of Collection instances
29+
const numbersJob = await db.createJob(() =>
30+
db.query(aql`FOR i IN 1..1000 RETURN i`)
31+
);
32+
// once loaded, numbersJob.result will be an ArrayCursor of numbers
33+
```
34+
1735
## [8.5.0] - 2023-10-09
1836

1937
### Added
@@ -1725,6 +1743,7 @@ For a detailed list of changes between pre-release versions of v7 see the
17251743

17261744
Graph methods now only return the relevant part of the response body.
17271745

1746+
[unreleased]: https://github.com/arangodb/arangojs/compare/v8.5.0...HEAD
17281747
[8.5.0]: https://github.com/arangodb/arangojs/compare/v8.4.1...v8.5.0
17291748
[8.4.1]: https://github.com/arangodb/arangojs/compare/v8.4.0...v8.4.1
17301749
[8.4.0]: https://github.com/arangodb/arangojs/compare/v8.3.1...v8.4.0

src/database.ts

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,6 +1755,17 @@ export type LogEntries = {
17551755
text: string[];
17561756
};
17571757

1758+
type TrappedError = {
1759+
error: true;
1760+
};
1761+
1762+
type TrappedRequest = {
1763+
error?: false;
1764+
jobId: string;
1765+
onResolve: (res: ArangojsResponse) => void;
1766+
onReject: (error: any) => void;
1767+
};
1768+
17581769
/**
17591770
* An object representing a single ArangoDB database. All arangojs collections,
17601771
* cursors, analyzers and so on are linked to a `Database` object.
@@ -1766,6 +1777,7 @@ export class Database {
17661777
protected _collections = new Map<string, Collection>();
17671778
protected _graphs = new Map<string, Graph>();
17681779
protected _views = new Map<string, View>();
1780+
protected _trapRequest?: (trapped: TrappedError | TrappedRequest) => void;
17691781

17701782
/**
17711783
* Creates a new `Database` instance with its own connection pool.
@@ -1905,6 +1917,49 @@ export class Database {
19051917
return new Route(this, path, headers);
19061918
}
19071919

1920+
/**
1921+
* Creates an async job by executing the given callback function. The first
1922+
* database request performed by the callback will be marked for asynchronous
1923+
* execution and its result will be made available as an async job.
1924+
*
1925+
* Returns a {@link Job} instance that can be used to retrieve the result
1926+
* of the callback function once the request has been executed.
1927+
*
1928+
* @param callback - Callback function to execute as an async job.
1929+
*
1930+
* @example
1931+
* ```js
1932+
* const db = new Database();
1933+
* const job = await db.createJob(() => db.collections());
1934+
* while (!job.isLoaded) {
1935+
* await timeout(1000);
1936+
* await job.load();
1937+
* }
1938+
* // job.result is a list of Collection instances
1939+
* ```
1940+
*/
1941+
async createJob<T>(callback: () => Promise<T>): Promise<Job<T>> {
1942+
const trap = new Promise<TrappedError | TrappedRequest>((resolveTrap) => {
1943+
this._trapRequest = (trapped) => resolveTrap(trapped);
1944+
});
1945+
const eventualResult = callback();
1946+
const trapped = await trap;
1947+
if (trapped.error) return eventualResult as Promise<any>;
1948+
const { jobId, onResolve, onReject } = trapped as TrappedRequest;
1949+
return new Job(
1950+
this,
1951+
jobId,
1952+
(res) => {
1953+
onResolve(res);
1954+
return eventualResult;
1955+
},
1956+
(e) => {
1957+
onReject(e);
1958+
return eventualResult;
1959+
}
1960+
);
1961+
}
1962+
19081963
/**
19091964
* @internal
19101965
*
@@ -1918,7 +1973,7 @@ export class Database {
19181973
* @param transform - An optional function to transform the low-level
19191974
* response object to a more useful return value.
19201975
*/
1921-
request<T = any>(
1976+
async request<T = any>(
19221977
options: RequestOptions & { absolutePath?: boolean },
19231978
transform?: (res: ArangojsResponse) => T
19241979
): Promise<T>;
@@ -1934,11 +1989,11 @@ export class Database {
19341989
* @param transform - If set to `false`, the raw response object will be
19351990
* returned.
19361991
*/
1937-
request(
1992+
async request(
19381993
options: RequestOptions & { absolutePath?: boolean },
19391994
transform: false
19401995
): Promise<ArangojsResponse>;
1941-
request<T = any>(
1996+
async request<T = any>(
19421997
{
19431998
absolutePath = false,
19441999
basePath,
@@ -1949,6 +2004,35 @@ export class Database {
19492004
if (!absolutePath) {
19502005
basePath = `/_db/${encodeURIComponent(this._name)}${basePath || ""}`;
19512006
}
2007+
if (this._trapRequest) {
2008+
const trap = this._trapRequest;
2009+
this._trapRequest = undefined;
2010+
return new Promise<T>(async (resolveRequest, rejectRequest) => {
2011+
const options = { ...opts };
2012+
options.headers = { ...options.headers, "x-arango-async": "store" };
2013+
let jobRes: ArangojsResponse;
2014+
try {
2015+
jobRes = await this._connection.request({ basePath, ...options });
2016+
} catch (e) {
2017+
trap({ error: true });
2018+
rejectRequest(e);
2019+
return;
2020+
}
2021+
const jobId = jobRes.headers["x-arango-async-id"] as string;
2022+
trap({
2023+
jobId,
2024+
onResolve: (res) => {
2025+
const result = transform ? transform(res) : (res as T);
2026+
resolveRequest(result);
2027+
return result;
2028+
},
2029+
onReject: (err) => {
2030+
rejectRequest(err);
2031+
throw err;
2032+
},
2033+
});
2034+
});
2035+
}
19522036
return this._connection.request(
19532037
{ basePath, ...opts },
19542038
transform || undefined

src/job.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
import { Database } from "./database";
2+
import { ArangojsResponse } from "./lib/request.node";
23

34
/**
45
* Represents an async job in a {@link database.Database}.
56
*/
67
export class Job<T = any> {
78
protected _id: string;
89
protected _db: Database;
10+
protected _transformResponse?: (res: ArangojsResponse) => Promise<T>;
11+
protected _transformError?: (error: any) => Promise<T>;
912
protected _loaded: boolean = false;
1013
protected _result: T | undefined;
1114

1215
/**
1316
* @internal
1417
*/
15-
constructor(db: Database, id: string) {
18+
constructor(
19+
db: Database,
20+
id: string,
21+
transformResponse?: (res: ArangojsResponse) => Promise<T>,
22+
transformError?: (error: any) => Promise<T>
23+
) {
1624
this._db = db;
1725
this._id = id;
26+
this._transformResponse = transformResponse;
27+
this._transformError = transformError;
1828
}
1929

2030
/**
@@ -49,16 +59,28 @@ export class Job<T = any> {
4959
*/
5060
async load(): Promise<T | undefined> {
5161
if (!this.isLoaded) {
52-
const res = await this._db.request(
53-
{
54-
method: "PUT",
55-
path: `/_api/job/${this._id}`,
56-
},
57-
false
58-
);
62+
let res: ArangojsResponse;
63+
try {
64+
res = await this._db.request(
65+
{
66+
method: "PUT",
67+
path: `/_api/job/${this._id}`,
68+
},
69+
false
70+
);
71+
} catch (e) {
72+
if (this._transformError) {
73+
return this._transformError(e);
74+
}
75+
throw e;
76+
}
5977
if (res.statusCode !== 204) {
6078
this._loaded = true;
61-
this._result = res.body;
79+
if (this._transformResponse) {
80+
this._result = await this._transformResponse(res);
81+
} else {
82+
this._result = res.body;
83+
}
6284
}
6385
}
6486
return this._result;

0 commit comments

Comments
 (0)