@oada/client
Advanced tools
Comparing version 2.5.1 to 2.6.0
/// <reference types="node" /> | ||
import { Buffer } from "buffer"; | ||
import type { EventEmitter } from "events"; | ||
import type { Json, Change } from "."; | ||
/** | ||
* @todo Support more than just Buffer? | ||
*/ | ||
export declare type Body = Json | Buffer; | ||
export interface ConnectionRequest { | ||
@@ -9,3 +14,3 @@ requestId?: string; | ||
headers: Record<string, string>; | ||
data?: Json; | ||
data?: Body; | ||
} | ||
@@ -17,3 +22,3 @@ export interface ConnectionResponse { | ||
headers: Record<string, string>; | ||
data: Json; | ||
data?: Body; | ||
} | ||
@@ -35,3 +40,5 @@ export interface ConnectionChange { | ||
token?: string; | ||
/** @default 1 */ | ||
concurrency?: number; | ||
/** @default "http" */ | ||
connection?: "ws" | "http" | Connection; | ||
@@ -46,16 +53,25 @@ } | ||
} | ||
/** | ||
* Watch whose callback gets single changes | ||
*/ | ||
export interface WatchRequestSingle { | ||
type?: "single"; | ||
path: string; | ||
rev?: string; | ||
rev?: number | string; | ||
watchCallback: (response: Readonly<Change>) => void; | ||
timeout?: number; | ||
} | ||
/** | ||
* Watch whose callback gets change trees | ||
*/ | ||
export interface WatchRequestTree { | ||
type: "tree"; | ||
path: string; | ||
rev?: string; | ||
rev?: number | string; | ||
watchCallback: (response: readonly Readonly<Change>[]) => void; | ||
timeout?: number; | ||
} | ||
/** | ||
* Discriminated union of watch for single changes or watch for change trees | ||
*/ | ||
export declare type WatchRequest = WatchRequestSingle | WatchRequestTree; | ||
@@ -85,29 +101,61 @@ export interface PUTRequest { | ||
} | ||
/** | ||
* @internal | ||
*/ | ||
export interface OADATree { | ||
_type: string; | ||
_type?: string; | ||
[k: string]: OADATree; | ||
} | ||
/** Main OADAClient class */ | ||
export declare class OADAClient { | ||
private _token; | ||
private _domain; | ||
private _concurrency; | ||
private _ws; | ||
private _watchList; | ||
private _renewedReqIdMap; | ||
constructor(config: Config); | ||
#private; | ||
constructor({ domain, token, concurrency, connection, }: Config); | ||
/** | ||
* Repurpose a existing connection to OADA-compliant server with a new token | ||
* @param token New token. | ||
*/ | ||
clone(token: string): OADAClient; | ||
/** | ||
* Get the connection token | ||
*/ | ||
getToken(): string; | ||
/** | ||
* Get the connection domain | ||
*/ | ||
getDomain(): string; | ||
/** Disconnect from server */ | ||
disconnect(): Promise<void>; | ||
/** Wait for the connection to open */ | ||
awaitConnection(): Promise<void>; | ||
/** | ||
* Send GET request | ||
* @param request request | ||
*/ | ||
get(request: GETRequest): Promise<Response>; | ||
/** | ||
* Set up watch | ||
* @param request watch request | ||
*/ | ||
watch(request: WatchRequest): Promise<string>; | ||
unwatch(requestId: string): Promise<Response>; | ||
private _recursiveGet; | ||
/** | ||
* Send PUT request | ||
* @param request PUT request | ||
*/ | ||
put(request: PUTRequest): Promise<Response>; | ||
/** | ||
* Send POST request | ||
* @param request PUT request | ||
*/ | ||
post(request: POSTRequest): Promise<Response>; | ||
/** | ||
* Send HEAD request | ||
* @param request HEAD request | ||
*/ | ||
head(request: HEADRequest): Promise<Response>; | ||
/** | ||
* Send DELETE request | ||
* @param request DELETE request | ||
*/ | ||
delete(request: DELETERequest): Promise<Response>; | ||
private _createResource; | ||
private _resourceExists; | ||
} |
@@ -21,5 +21,17 @@ "use strict"; | ||
}; | ||
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
if (kind === "m") throw new TypeError("Private method is not writable"); | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); | ||
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; | ||
}; | ||
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); | ||
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
var _OADAClient_instances, _OADAClient_token, _OADAClient_domain, _OADAClient_concurrency, _OADAClient_ws, _OADAClient_watchList, _OADAClient_renewedReqIdMap, _OADAClient_recursiveGet, _OADAClient_createResource, _OADAClient_resourceExists; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -30,2 +42,3 @@ exports.OADAClient = void 0; | ||
const debug_1 = __importDefault(require("debug")); | ||
const buffer_1 = require("buffer"); | ||
const utils = __importStar(require("./utils")); | ||
@@ -35,72 +48,110 @@ const websocket_1 = require("./websocket"); | ||
const trace = debug_1.default("@oada/client:client:trace"); | ||
/** Main OADAClient class */ | ||
class OADAClient { | ||
constructor(config) { | ||
this._token = ""; | ||
this._domain = ""; | ||
this._concurrency = 1; | ||
this._domain = config.domain.replace(/^https:\/\//, ""); | ||
this._token = config.token || this._token; | ||
this._concurrency = config.concurrency || this._concurrency; | ||
this._watchList = new Map(); | ||
this._renewedReqIdMap = new Map(); | ||
if (!config.connection || config.connection === "ws") { | ||
this._ws = new websocket_1.WebSocketClient(this._domain, this._concurrency); | ||
constructor({ domain, token = "", concurrency = 1, connection = "http", }) { | ||
_OADAClient_instances.add(this); | ||
_OADAClient_token.set(this, void 0); | ||
_OADAClient_domain.set(this, void 0); | ||
_OADAClient_concurrency.set(this, void 0); | ||
_OADAClient_ws.set(this, void 0); | ||
_OADAClient_watchList.set(this, void 0); // currentRequestId -> WatchRequest | ||
_OADAClient_renewedReqIdMap.set(this, void 0); // currentRequestId -> originalRequestId | ||
// help for those who can't remember if https should be there | ||
__classPrivateFieldSet(this, _OADAClient_domain, domain.replace(/^https:\/\//, ""), "f"); | ||
__classPrivateFieldSet(this, _OADAClient_token, token, "f"); | ||
__classPrivateFieldSet(this, _OADAClient_concurrency, concurrency, "f"); | ||
__classPrivateFieldSet(this, _OADAClient_watchList, new Map(), "f"); | ||
__classPrivateFieldSet(this, _OADAClient_renewedReqIdMap, new Map(), "f"); | ||
if (connection === "ws") { | ||
__classPrivateFieldSet(this, _OADAClient_ws, new websocket_1.WebSocketClient(__classPrivateFieldGet(this, _OADAClient_domain, "f"), __classPrivateFieldGet(this, _OADAClient_concurrency, "f")), "f"); | ||
} | ||
else if (config.connection === "http") { | ||
this._ws = new http_1.HttpClient(this._domain, this._token, this._concurrency); | ||
else if (connection === "http") { | ||
__classPrivateFieldSet(this, _OADAClient_ws, new http_1.HttpClient(__classPrivateFieldGet(this, _OADAClient_domain, "f"), __classPrivateFieldGet(this, _OADAClient_token, "f"), __classPrivateFieldGet(this, _OADAClient_concurrency, "f")), "f"); | ||
} | ||
else { | ||
this._ws = config.connection; | ||
// Otherwise, they gave us a WebSocketClient to use | ||
__classPrivateFieldSet(this, _OADAClient_ws, connection, "f"); | ||
} | ||
this._ws.on("open", async () => { | ||
const prevWatchList = this._watchList; | ||
this._watchList = new Map(); | ||
/* Register handler for the "open" event. | ||
This event is emitted when 1) this is an initial connection, or 2) the websocket is reconnected. | ||
For the initial connection, no special action is needed. | ||
z For the reconnection case, we need to re-establish the watches. */ | ||
__classPrivateFieldGet(this, _OADAClient_ws, "f").on("open", async () => { | ||
const prevWatchList = __classPrivateFieldGet(this, _OADAClient_watchList, "f"); | ||
__classPrivateFieldSet(this, _OADAClient_watchList, new Map(), "f"); | ||
for (const [oldRequestId, watchRequest] of prevWatchList.entries()) { | ||
// Re-establish watch | ||
const newRequestId = await this.watch(watchRequest); | ||
const originalRequestId = this._renewedReqIdMap.get(oldRequestId); | ||
// If requestId had been already renewed, keep the original requestId so that unwatch() can use that | ||
const originalRequestId = __classPrivateFieldGet(this, _OADAClient_renewedReqIdMap, "f").get(oldRequestId); | ||
if (originalRequestId) { | ||
this._renewedReqIdMap.set(newRequestId, originalRequestId); | ||
this._renewedReqIdMap.delete(oldRequestId); | ||
__classPrivateFieldGet(this, _OADAClient_renewedReqIdMap, "f").set(newRequestId, originalRequestId); | ||
__classPrivateFieldGet(this, _OADAClient_renewedReqIdMap, "f").delete(oldRequestId); | ||
} | ||
else { | ||
this._renewedReqIdMap.set(newRequestId, oldRequestId); | ||
__classPrivateFieldGet(this, _OADAClient_renewedReqIdMap, "f").set(newRequestId, oldRequestId); | ||
} | ||
trace(`Update requestId: ${oldRequestId} -> ${newRequestId}`); | ||
// Debug message | ||
trace("Update requestId: %s -> %s", oldRequestId, newRequestId); | ||
} | ||
}); | ||
} | ||
/** | ||
* Repurpose a existing connection to OADA-compliant server with a new token | ||
* @param token New token. | ||
*/ | ||
clone(token) { | ||
const c = new OADAClient({ | ||
domain: this._domain, | ||
domain: __classPrivateFieldGet(this, _OADAClient_domain, "f"), | ||
token: token, | ||
concurrency: this._concurrency, | ||
connection: this._ws, | ||
concurrency: __classPrivateFieldGet(this, _OADAClient_concurrency, "f"), | ||
// Reuse existing WS connection | ||
connection: __classPrivateFieldGet(this, _OADAClient_ws, "f"), | ||
}); | ||
return c; | ||
} | ||
/** | ||
* Get the connection token | ||
*/ | ||
getToken() { | ||
return this._token; | ||
return __classPrivateFieldGet(this, _OADAClient_token, "f"); | ||
} | ||
/** | ||
* Get the connection domain | ||
*/ | ||
getDomain() { | ||
return this._domain; | ||
return __classPrivateFieldGet(this, _OADAClient_domain, "f"); | ||
} | ||
/** Disconnect from server */ | ||
disconnect() { | ||
return this._ws.disconnect(); | ||
// close | ||
return __classPrivateFieldGet(this, _OADAClient_ws, "f").disconnect(); | ||
} | ||
/** Wait for the connection to open */ | ||
awaitConnection() { | ||
return this._ws.awaitConnection(); | ||
return __classPrivateFieldGet(this, _OADAClient_ws, "f").awaitConnection(); | ||
} | ||
/** | ||
* Send GET request | ||
* @param request request | ||
*/ | ||
async get(request) { | ||
const topLevelResponse = await this._ws.request({ | ||
// === Top-level GET === | ||
const topLevelResponse = await __classPrivateFieldGet(this, _OADAClient_ws, "f").request({ | ||
method: "get", | ||
headers: { | ||
authorization: `Bearer ${this._token}`, | ||
authorization: `Bearer ${__classPrivateFieldGet(this, _OADAClient_token, "f")}`, | ||
}, | ||
path: request.path, | ||
}, undefined, request.timeout); | ||
}, undefined, // omitting an optional parameter | ||
request.timeout); | ||
// === Recursive GET === | ||
if (request.tree) { | ||
// Get subtree | ||
const arrayPath = utils.toArrayPath(request.path); | ||
const subTree = utils.getObjectAtPath(request.tree, arrayPath); | ||
topLevelResponse.data = await this._recursiveGet(request.path, subTree, topLevelResponse.data || {}); | ||
// Replace "data" with the recursive GET result | ||
topLevelResponse.data = await __classPrivateFieldGet(this, _OADAClient_instances, "m", _OADAClient_recursiveGet).call(this, request.path, subTree, topLevelResponse.data || {}); | ||
} | ||
// === Register Watch === | ||
if (request.watchCallback) { | ||
@@ -116,12 +167,17 @@ const rev = topLevelResponse.headers | ||
} | ||
// Return top-level response | ||
return topLevelResponse; | ||
} | ||
/** | ||
* Set up watch | ||
* @param request watch request | ||
*/ | ||
async watch(request) { | ||
const headers = {}; | ||
if (typeof request.rev !== "undefined") { | ||
headers["x-oada-rev"] = request.rev; | ||
headers["x-oada-rev"] = request.rev + ""; | ||
} | ||
const r = await this._ws.request({ | ||
const r = await __classPrivateFieldGet(this, _OADAClient_ws, "f").request({ | ||
method: "watch", | ||
headers: Object.assign({ authorization: `Bearer ${this._token}` }, headers), | ||
headers: Object.assign({ authorization: `Bearer ${__classPrivateFieldGet(this, _OADAClient_token, "f")}` }, headers), | ||
path: request.path, | ||
@@ -138,3 +194,3 @@ }, (resp) => { | ||
if (change.path === "") { | ||
const watchRequest = this._watchList.get(resp.requestId[0]); | ||
const watchRequest = __classPrivateFieldGet(this, _OADAClient_watchList, "f").get(resp.requestId[0]); | ||
if (watchRequest) { | ||
@@ -144,3 +200,3 @@ const newRev = (_a = change.body) === null || _a === void 0 ? void 0 : _a["_rev"]; | ||
watchRequest.rev = newRev; | ||
trace(`Updated the rev of request ${resp.requestId[0]} to ${newRev}`); | ||
trace("Updated the rev of request %s to %s", resp.requestId[0], newRev); | ||
} | ||
@@ -160,11 +216,15 @@ else { | ||
} | ||
// Get requestId from the response | ||
const requestId = Array.isArray(r.requestId) | ||
? r.requestId[0] | ||
: r.requestId; | ||
this._watchList.set(requestId, request); | ||
: r.requestId; // server should always return an array requestId | ||
// Save watch request | ||
__classPrivateFieldGet(this, _OADAClient_watchList, "f").set(requestId, request); | ||
return requestId; | ||
} | ||
async unwatch(requestId) { | ||
// Retrieve the original requestId if it had been renewed | ||
// TODO: better way to do this? | ||
let activeRequestId = requestId; | ||
for (const [currentRequestId, originalRequestId,] of this._renewedReqIdMap.entries()) { | ||
for (const [currentRequestId, originalRequestId,] of __classPrivateFieldGet(this, _OADAClient_renewedReqIdMap, "f").entries()) { | ||
if (originalRequestId === requestId) { | ||
@@ -174,4 +234,4 @@ activeRequestId = currentRequestId; | ||
} | ||
trace(`Unwatch requestId=${requestId}, actual=${activeRequestId}`); | ||
const response = await this._ws.request({ | ||
trace("Unwatch requestId=%s, actual=%s", requestId, activeRequestId); | ||
const response = await __classPrivateFieldGet(this, _OADAClient_ws, "f").request({ | ||
path: "", | ||
@@ -184,59 +244,40 @@ headers: { | ||
}); | ||
if (!this._watchList.delete(activeRequestId)) { | ||
// TODO: add timeout | ||
// Remove watch state info (this should always exist) | ||
if (!__classPrivateFieldGet(this, _OADAClient_watchList, "f").delete(activeRequestId)) { | ||
throw new Error("Could not find watch state information."); | ||
} | ||
this._renewedReqIdMap.delete(activeRequestId); | ||
// Remove renewed requestId data | ||
// (this may not exist if requestId has not been renewed) | ||
__classPrivateFieldGet(this, _OADAClient_renewedReqIdMap, "f").delete(activeRequestId); | ||
return response; | ||
} | ||
async _recursiveGet(path, subTree, data) { | ||
if (!subTree || !data) { | ||
throw new Error("Path mismatch."); | ||
} | ||
if (subTree["_type"]) { | ||
data = (await this.get({ path })).data || {}; | ||
} | ||
let children; | ||
if (subTree["*"]) { | ||
children = Object.keys(data).reduce((acc, key) => { | ||
if (data && typeof data[key] === "object") { | ||
acc.push({ treeKey: "*", dataKey: key }); | ||
} | ||
return acc; | ||
}, []); | ||
} | ||
else { | ||
children = Object.keys(subTree || {}).reduce((acc, key) => { | ||
if (data && typeof data[key] === "object") { | ||
acc.push({ treeKey: key, dataKey: key }); | ||
} | ||
return acc; | ||
}, []); | ||
} | ||
const promises = children.map(async (item) => { | ||
const childPath = path + "/" + item.dataKey; | ||
if (!data) { | ||
return; | ||
} | ||
const res = await this._recursiveGet(childPath, subTree[item.treeKey], data[item.dataKey]); | ||
data[item.dataKey] = res; | ||
return; | ||
}); | ||
return await Promise.all(promises).then(() => { | ||
return data; | ||
}); | ||
} | ||
/** | ||
* Send PUT request | ||
* @param request PUT request | ||
*/ | ||
async put(request) { | ||
// convert string path to array | ||
// (e.g., /bookmarks/abc/def -> ['bookmarks', 'abc', 'def']) | ||
const pathArray = utils.toArrayPath(request.path); | ||
if (request.tree) { | ||
// Retry counter | ||
let retryCount = 0; | ||
// link object (eventually substituted by an actual link object) | ||
let linkObj = null; | ||
let newResourcePathArray = []; | ||
for (let i = pathArray.length - 1; i >= 0; i--) { | ||
// get current path | ||
const partialPathArray = pathArray.slice(0, i + 1); | ||
// get corresponding data definition from the provided tree | ||
const treeObj = utils.getObjectAtPath(request.tree, partialPathArray); | ||
if ("_type" in treeObj) { | ||
// it's a resource | ||
const contentType = treeObj["_type"]; | ||
const partialPath = utils.toStringPath(partialPathArray); | ||
const resourceCheckResult = await this._resourceExists(partialPath); | ||
// check if resource already exists on the remote server | ||
const resourceCheckResult = await __classPrivateFieldGet(this, _OADAClient_instances, "m", _OADAClient_resourceExists).call(this, partialPath); | ||
if (resourceCheckResult.exist) { | ||
// CASE 1: resource exists on server. | ||
// simply create a link using PUT request | ||
if (linkObj && newResourcePathArray.length > 0) { | ||
@@ -247,2 +288,3 @@ const linkPutResponse = await this.put({ | ||
data: linkObj, | ||
// Ensure the resource has not been modified (opportunistic lock) | ||
revIfMatch: resourceCheckResult.rev, | ||
@@ -254,8 +296,11 @@ }).catch((msg) => { | ||
else { | ||
throw new Error(`Error: ${msg.statusText}`); | ||
throw new Error(msg.statusText); | ||
} | ||
}); | ||
// Handle return code 412 (If-Match failed) | ||
if (linkPutResponse.status == 412) { | ||
// Retry with exponential backoff | ||
if (retryCount++ < 5) { | ||
await utils.delay(100 * (retryCount * retryCount + Math.random())); | ||
// Reset loop counter and do tree construction again. | ||
i = pathArray.length; | ||
@@ -269,5 +314,9 @@ continue; | ||
} | ||
// We hit a resource that already exists. | ||
// No need to further traverse the tree. | ||
break; | ||
} | ||
else { | ||
// CASE 2: resource does NOT exist on server. | ||
// create a new nested object containing a link | ||
const relativePathArray = newResourcePathArray.slice(i + 1); | ||
@@ -277,8 +326,10 @@ const newResource = linkObj | ||
: {}; | ||
const resourceId = await this._createResource(contentType, newResource); | ||
// create a new resource | ||
const resourceId = await __classPrivateFieldGet(this, _OADAClient_instances, "m", _OADAClient_createResource).call(this, contentType, newResource); | ||
// save a link | ||
linkObj = | ||
"_rev" in treeObj | ||
? { _id: resourceId, _rev: 0 } | ||
: { _id: resourceId }; | ||
newResourcePathArray = partialPathArray.slice(); | ||
? { _id: resourceId, _rev: 0 } // versioned link | ||
: { _id: resourceId }; // non-versioned link | ||
newResourcePathArray = partialPathArray.slice(); // clone | ||
} | ||
@@ -288,10 +339,12 @@ } | ||
} | ||
const contentType = request.contentType || | ||
(request.data && request.data["_type"]) || | ||
// Get content-type | ||
const contentType = request.contentType || // 1) get content-type from the argument | ||
(request.data && request.data["_type"]) || // 2) get content-type from the resource body | ||
(request.tree | ||
? utils.getObjectAtPath(request.tree, pathArray)["_type"] | ||
: "application/json"); | ||
return this._ws.request({ | ||
? utils.getObjectAtPath(request.tree, pathArray)["_type"] // 3) get content-type from the tree | ||
: "application/json"); // 4) Assume application/json | ||
// return PUT response | ||
return __classPrivateFieldGet(this, _OADAClient_ws, "f").request({ | ||
method: "put", | ||
headers: Object.assign({ authorization: `Bearer ${this._token}`, "content-type": contentType }, (request.revIfMatch && { | ||
headers: Object.assign({ authorization: `Bearer ${__classPrivateFieldGet(this, _OADAClient_token, "f")}`, "content-type": contentType }, (request.revIfMatch && { | ||
"if-match": request.revIfMatch.toString(), | ||
@@ -301,8 +354,17 @@ })), | ||
data: request.data, | ||
}, undefined, request.timeout); | ||
}, undefined, // omitting an optional parameter | ||
request.timeout); | ||
} | ||
/** | ||
* Send POST request | ||
* @param request PUT request | ||
*/ | ||
async post(request) { | ||
// convert string path to array | ||
// (e.g., /bookmarks/abc/def -> ['bookmarks', 'abc', 'def']) | ||
const pathArray = utils.toArrayPath(request.path); | ||
const data = request.data; | ||
if (request.tree) { | ||
// We could go to all the trouble of re-implementing tree puts for posts, | ||
// but it's much easier to just make a ksuid and do the tree put | ||
const newkey = (await ksuid_1.default.random()).string; | ||
@@ -312,11 +374,13 @@ request.path += `/${newkey}`; | ||
} | ||
const contentType = request.contentType || | ||
(request.data && request.data["_type"]) || | ||
// Get content-type | ||
const contentType = request.contentType || // 1) get content-type from the argument | ||
(request.data && request.data["_type"]) || // 2) get content-type from the resource body | ||
(request.tree | ||
? utils.getObjectAtPath(request.tree, pathArray)["_type"] | ||
: "application/json"); | ||
return this._ws.request({ | ||
? utils.getObjectAtPath(request.tree, pathArray)["_type"] // 3) get content-type from the tree | ||
: "application/json"); // 4) Assume application/json | ||
// return PUT response | ||
return __classPrivateFieldGet(this, _OADAClient_ws, "f").request({ | ||
method: "post", | ||
headers: { | ||
authorization: `Bearer ${this._token}`, | ||
authorization: `Bearer ${__classPrivateFieldGet(this, _OADAClient_token, "f")}`, | ||
"content-type": contentType, | ||
@@ -326,60 +390,136 @@ }, | ||
data, | ||
}, undefined, request.timeout); | ||
}, undefined, // omitting an optional parameter | ||
request.timeout); | ||
} | ||
/** | ||
* Send HEAD request | ||
* @param request HEAD request | ||
*/ | ||
async head(request) { | ||
return this._ws.request({ | ||
// return HEAD response | ||
return __classPrivateFieldGet(this, _OADAClient_ws, "f").request({ | ||
method: "head", | ||
headers: { | ||
authorization: `Bearer ${this._token}`, | ||
authorization: `Bearer ${__classPrivateFieldGet(this, _OADAClient_token, "f")}`, | ||
}, | ||
path: request.path, | ||
}, undefined, request.timeout); | ||
}, undefined, // omitting an optional parameter | ||
request.timeout); | ||
} | ||
/** | ||
* Send DELETE request | ||
* @param request DELETE request | ||
*/ | ||
async delete(request) { | ||
return this._ws.request({ | ||
// return HEAD response | ||
return __classPrivateFieldGet(this, _OADAClient_ws, "f").request({ | ||
method: "delete", | ||
headers: { | ||
authorization: `Bearer ${this._token}`, | ||
authorization: `Bearer ${__classPrivateFieldGet(this, _OADAClient_token, "f")}`, | ||
}, | ||
path: request.path, | ||
}, undefined, request.timeout); | ||
}, undefined, // omitting an optional parameter | ||
request.timeout); | ||
} | ||
async _createResource(contentType, data) { | ||
const resourceId = "resources/" + ksuid_1.default.randomSync().string; | ||
await this.put({ | ||
path: "/" + resourceId, | ||
data, | ||
contentType, | ||
}); | ||
return resourceId; | ||
} | ||
exports.OADAClient = OADAClient; | ||
_OADAClient_token = new WeakMap(), _OADAClient_domain = new WeakMap(), _OADAClient_concurrency = new WeakMap(), _OADAClient_ws = new WeakMap(), _OADAClient_watchList = new WeakMap(), _OADAClient_renewedReqIdMap = new WeakMap(), _OADAClient_instances = new WeakSet(), _OADAClient_recursiveGet = | ||
// GET resource recursively | ||
async function _OADAClient_recursiveGet(path, subTree, data) { | ||
// If either subTree or data does not exist, there's mismatch between | ||
// the provided tree and the actual data stored on the server | ||
if (!subTree || !data) { | ||
throw new Error("Path mismatch."); | ||
} | ||
async _resourceExists(path) { | ||
if (path === "/resources") { | ||
return { exist: true }; | ||
// if the object is a link to another resource (i.e., contains "_type"), | ||
// then perform GET | ||
if (subTree["_type"]) { | ||
data = (await this.get({ path })).data || {}; | ||
} | ||
// TODO: should this error? | ||
if (buffer_1.Buffer.isBuffer(data)) { | ||
return data; | ||
} | ||
// select children to traverse | ||
const children = []; | ||
if (subTree["*"]) { | ||
// If "*" is specified in the tree provided by the user, | ||
// get all children from the server | ||
for (const key of Object.keys(data)) { | ||
if (typeof data[key] === "object") { | ||
children.push({ treeKey: "*", dataKey: key }); | ||
} | ||
} | ||
const headResponse = await this.head({ | ||
path, | ||
}).catch((msg) => { | ||
if (msg.status == 404) { | ||
return msg; | ||
} | ||
else { | ||
// Otherwise, get children from the tree provided by the user | ||
for (const key of Object.keys(subTree || {})) { | ||
if (typeof data[key] === "object") { | ||
children.push({ treeKey: key, dataKey: key }); | ||
} | ||
else if (msg.status == 403 && path.match(/^\/resources/)) { | ||
return { status: 404 }; | ||
} | ||
else { | ||
throw new Error(`Error: head for resource returned ${msg.statusText || msg}`); | ||
} | ||
}); | ||
if (headResponse.status == 200) { | ||
return { exist: true, rev: headResponse.headers["x-oada-rev"] }; | ||
} | ||
else if (headResponse.status == 404) { | ||
return { exist: false }; | ||
} | ||
// initiate recursive calls | ||
const promises = children.map(async (item) => { | ||
const childPath = path + "/" + item.dataKey; | ||
if (!data) { | ||
return; | ||
} | ||
const res = await __classPrivateFieldGet(this, _OADAClient_instances, "m", _OADAClient_recursiveGet).call(this, childPath, subTree[item.treeKey], data[item.dataKey]); | ||
// @ts-ignore | ||
data[item.dataKey] = res; | ||
return; | ||
}); | ||
await Promise.all(promises); | ||
return data; // return object at "path" | ||
}, _OADAClient_createResource = | ||
/** Create a new resource. Returns resource ID */ | ||
async function _OADAClient_createResource(contentType, data) { | ||
// Create unique resource ID | ||
const resourceId = "resources/" + ksuid_1.default.randomSync().string; | ||
// append resource ID and content type to object | ||
// const fullData = { _id: resourceId, _type: contentType, ...data }; | ||
// send PUT request | ||
await this.put({ | ||
path: "/" + resourceId, | ||
data, | ||
contentType, | ||
}); | ||
// return resource ID | ||
return resourceId; | ||
}, _OADAClient_resourceExists = | ||
/** check if the specified path exists. Returns boolean value. */ | ||
async function _OADAClient_resourceExists(path) { | ||
// In tree put to /resources, the top-level "/resources" should | ||
// look like it exists, even though oada doesn't allow GET on /resources | ||
// directly. | ||
if (path === "/resources") { | ||
return { exist: true }; | ||
} | ||
// Otherwise, send HEAD request for resource | ||
const headResponse = await this.head({ | ||
path, | ||
}).catch((msg) => { | ||
if (msg.status == 404) { | ||
return msg; | ||
} | ||
else if (msg.status == 403 && path.match(/^\/resources/)) { | ||
// 403 is what you get on resources that don't exist (i.e. Forbidden) | ||
return { status: 404 }; | ||
} | ||
else { | ||
throw Error("Status code is neither 200 nor 404."); | ||
throw new Error(`Error: head for resource returned ${msg.statusText || msg}`); | ||
} | ||
}); | ||
// check status value | ||
if (headResponse.status == 200) { | ||
return { exist: true, rev: headResponse.headers["x-oada-rev"] }; | ||
} | ||
} | ||
exports.OADAClient = OADAClient; | ||
else if (headResponse.status == 404) { | ||
return { exist: false }; | ||
} | ||
else { | ||
throw Error("Status code is neither 200 nor 404."); | ||
} | ||
}; | ||
//# sourceMappingURL=client.js.map |
@@ -0,2 +1,12 @@ | ||
/** | ||
* Stuff for having client handle "recoverable" errors | ||
* rather than passing everything up to the user | ||
* | ||
* @packageDocumentation | ||
*/ | ||
import type { ConnectionResponse } from "./client"; | ||
/** | ||
* Handle any errors that client can deal with, | ||
* otherwise reject with original error. | ||
*/ | ||
export declare function handleErrors<R extends unknown[]>(req: (...args: R) => Promise<ConnectionResponse>, ...args: R): Promise<ConnectionResponse>; |
"use strict"; | ||
/** | ||
* Stuff for having client handle "recoverable" errors | ||
* rather than passing everything up to the user | ||
* | ||
* @packageDocumentation | ||
*/ | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -12,5 +18,18 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
const trace = debug_1.default("@oada/client:errors:trace"); | ||
/** | ||
* Wait 5 minutes if 429 with no Retry-After header | ||
* | ||
* @todo add override for this in client config? | ||
*/ | ||
const DEFAULT_RETY_TIMEOUT = 5 * 60 * 10000; | ||
/** | ||
* Handle rate limit errors | ||
* | ||
* Wait the length specified by Retry-After header, | ||
* or `DEFAULT_RETY_TIMEOUT` if the header is not present. | ||
*/ | ||
async function handleRatelimit(err, req, ...args) { | ||
const headers = new fetch_1.Headers(err.headers); | ||
// Figure out how many ms to wait | ||
// Header is either number of seconds, or a date/time | ||
const retry = headers.get("Retry-After"); | ||
@@ -24,2 +43,6 @@ const timeout = retry | ||
} | ||
/** | ||
* Handle any errors that client can deal with, | ||
* otherwise reject with original error. | ||
*/ | ||
async function handleErrors(req, ...args) { | ||
@@ -31,2 +54,3 @@ var _a, _b, _c, _d; | ||
catch (err) { | ||
// TODO: WTF why is error an array sometimes??? | ||
const e = (_d = (_c = (_b = (_a = err === null || err === void 0 ? void 0 : err[0]) === null || _a === void 0 ? void 0 : _a.error) !== null && _b !== void 0 ? _b : err === null || err === void 0 ? void 0 : err[0]) !== null && _c !== void 0 ? _c : err === null || err === void 0 ? void 0 : err.error) !== null && _d !== void 0 ? _d : err; | ||
@@ -37,2 +61,3 @@ trace(e, "Attempting to handle error"); | ||
return await handleRatelimit(e, req, ...args); | ||
// Some servers use 503 for rate limit... | ||
case 503: { | ||
@@ -43,4 +68,6 @@ const headers = new fetch_1.Headers(e.headers); | ||
} | ||
// If no Retry-After, don't assume rate-limit? | ||
} | ||
} | ||
// Pass error up | ||
throw err; | ||
@@ -47,0 +74,0 @@ } |
@@ -5,2 +5,3 @@ "use strict"; | ||
const fetch_h2_1 = require("fetch-h2"); | ||
// Create our own context to honor NODE_TLS_REJECT_UNAUTHORIZED like https | ||
const context = () => fetch_h2_1.context({ | ||
@@ -14,3 +15,4 @@ session: { | ||
Object.defineProperty(exports, "Headers", { enumerable: true, get: function () { return cross_fetch_1.Headers; } }); | ||
// cross-fetch has fetch as default export | ||
exports.default = fetch_h2_1.fetch; | ||
//# sourceMappingURL=fetch.js.map |
@@ -5,16 +5,18 @@ /// <reference types="node" /> | ||
export declare class HttpClient extends EventEmitter implements Connection { | ||
private _domain; | ||
private _token; | ||
private _status; | ||
private _q; | ||
private initialConnection; | ||
private concurrency; | ||
private context; | ||
private ws?; | ||
#private; | ||
/** | ||
* Constructor | ||
* @param domain Domain. E.g., www.example.com | ||
* @param concurrency Number of allowed in-flight requests. Default 10. | ||
*/ | ||
constructor(domain: string, token: string, concurrency?: number); | ||
/** Disconnect the connection */ | ||
disconnect(): Promise<void>; | ||
/** Return true if connected, otherwise false */ | ||
isConnected(): boolean; | ||
/** Wait for the connection to open */ | ||
awaitConnection(): Promise<void>; | ||
request(req: ConnectionRequest, callback?: (response: Readonly<ConnectionChange>) => void, timeout?: number): Promise<ConnectionResponse>; | ||
/** send a request to server */ | ||
private doRequest; | ||
} |
128
dist/http.js
@@ -21,5 +21,17 @@ "use strict"; | ||
}; | ||
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
if (kind === "m") throw new TypeError("Private method is not writable"); | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); | ||
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; | ||
}; | ||
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); | ||
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
var _HttpClient_domain, _HttpClient_token, _HttpClient_status, _HttpClient_q, _HttpClient_initialConnection, _HttpClient_concurrency, _HttpClient_context, _HttpClient_ws; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -32,2 +44,3 @@ exports.HttpClient = void 0; | ||
const debug_1 = __importDefault(require("debug")); | ||
const type_is_1 = __importDefault(require("type-is")); | ||
const websocket_1 = require("./websocket"); | ||
@@ -45,14 +58,30 @@ const errors_1 = require("./errors"); | ||
class HttpClient extends events_1.EventEmitter { | ||
/** | ||
* Constructor | ||
* @param domain Domain. E.g., www.example.com | ||
* @param concurrency Number of allowed in-flight requests. Default 10. | ||
*/ | ||
constructor(domain, token, concurrency = 10) { | ||
super(); | ||
this.context = fetch_1.context ? fetch_1.context() : { fetch: fetch_1.default }; | ||
this._domain = domain.match(/^http/) ? domain : `https://${domain}`; | ||
this._domain = this._domain.replace(/\/$/, ""); | ||
this._token = token; | ||
this._status = ConnectionStatus.Connecting; | ||
trace("Opening HTTP connection to HEAD %s/bookmarks w/authorization: Bearer %s", this._domain, this._token); | ||
this.initialConnection = this.context | ||
.fetch(`${this._domain}/bookmarks`, { | ||
_HttpClient_domain.set(this, void 0); | ||
_HttpClient_token.set(this, void 0); | ||
_HttpClient_status.set(this, void 0); | ||
_HttpClient_q.set(this, void 0); | ||
_HttpClient_initialConnection.set(this, void 0); // await on the initial HEAD | ||
_HttpClient_concurrency.set(this, void 0); | ||
_HttpClient_context.set(this, void 0); | ||
_HttpClient_ws.set(this, void 0); // Fall-back socket for watches | ||
__classPrivateFieldSet(this, _HttpClient_context, fetch_1.context ? fetch_1.context() : { fetch: fetch_1.default }, "f"); | ||
// ensure leading https:// | ||
__classPrivateFieldSet(this, _HttpClient_domain, domain.match(/^http/) ? domain : `https://${domain}`, "f"); | ||
// ensure no trailing slash | ||
__classPrivateFieldSet(this, _HttpClient_domain, __classPrivateFieldGet(this, _HttpClient_domain, "f").replace(/\/$/, ""), "f"); | ||
__classPrivateFieldSet(this, _HttpClient_token, token, "f"); | ||
__classPrivateFieldSet(this, _HttpClient_status, ConnectionStatus.Connecting, "f"); | ||
// "Open" the http connection: just make sure a HEAD succeeds | ||
trace("Opening HTTP connection to HEAD %s/bookmarks w/authorization: Bearer %s", __classPrivateFieldGet(this, _HttpClient_domain, "f"), __classPrivateFieldGet(this, _HttpClient_token, "f")); | ||
__classPrivateFieldSet(this, _HttpClient_initialConnection, __classPrivateFieldGet(this, _HttpClient_context, "f") | ||
.fetch(`${__classPrivateFieldGet(this, _HttpClient_domain, "f")}/bookmarks`, { | ||
method: "HEAD", | ||
headers: { authorization: `Bearer ${this._token}` }, | ||
headers: { authorization: `Bearer ${__classPrivateFieldGet(this, _HttpClient_token, "f")}` }, | ||
}) | ||
@@ -63,3 +92,3 @@ .then((result) => { | ||
trace('Initial HEAD succeeded, emitting "open"'); | ||
this._status = ConnectionStatus.Connected; | ||
__classPrivateFieldSet(this, _HttpClient_status, ConnectionStatus.Connected, "f"); | ||
this.emit("open"); | ||
@@ -69,36 +98,44 @@ } | ||
trace('Initial HEAD failed, emitting "close"'); | ||
this._status = ConnectionStatus.Disconnected; | ||
__classPrivateFieldSet(this, _HttpClient_status, ConnectionStatus.Disconnected, "f"); | ||
this.emit("close"); | ||
} | ||
}), "f"); | ||
__classPrivateFieldSet(this, _HttpClient_concurrency, concurrency, "f"); | ||
__classPrivateFieldSet(this, _HttpClient_q, new p_queue_1.default({ concurrency }), "f"); | ||
__classPrivateFieldGet(this, _HttpClient_q, "f").on("active", () => { | ||
trace(`HTTP Queue. Size: ${__classPrivateFieldGet(this, _HttpClient_q, "f").size} pending: ${__classPrivateFieldGet(this, _HttpClient_q, "f").pending}`); | ||
}); | ||
this.concurrency = concurrency; | ||
this._q = new p_queue_1.default({ concurrency }); | ||
this._q.on("active", () => { | ||
trace(`HTTP Queue. Size: ${this._q.size} pending: ${this._q.pending}`); | ||
}); | ||
} | ||
/** Disconnect the connection */ | ||
async disconnect() { | ||
var _a, _b, _c; | ||
this._status = ConnectionStatus.Disconnected; | ||
await ((_b = (_a = this.context).disconnectAll) === null || _b === void 0 ? void 0 : _b.call(_a)); | ||
await ((_c = this.ws) === null || _c === void 0 ? void 0 : _c.disconnect()); | ||
__classPrivateFieldSet(this, _HttpClient_status, ConnectionStatus.Disconnected, "f"); | ||
// Close our connections | ||
await ((_b = (_a = __classPrivateFieldGet(this, _HttpClient_context, "f")).disconnectAll) === null || _b === void 0 ? void 0 : _b.call(_a)); | ||
// Close our ws connection | ||
await ((_c = __classPrivateFieldGet(this, _HttpClient_ws, "f")) === null || _c === void 0 ? void 0 : _c.disconnect()); | ||
this.emit("close"); | ||
} | ||
/** Return true if connected, otherwise false */ | ||
isConnected() { | ||
return this._status == ConnectionStatus.Connected; | ||
return __classPrivateFieldGet(this, _HttpClient_status, "f") == ConnectionStatus.Connected; | ||
} | ||
/** Wait for the connection to open */ | ||
async awaitConnection() { | ||
await this.initialConnection; | ||
// Wait for the initial HEAD request to return | ||
await __classPrivateFieldGet(this, _HttpClient_initialConnection, "f"); | ||
} | ||
// TODO: Add support for WATCH via h2 push and/or RFC 8441 | ||
async request(req, callback, timeout) { | ||
var _a; | ||
trace(req, "Starting http request"); | ||
// Check for WATCH/UNWATCH | ||
if (req.method === "watch" || req.method === "unwatch" || callback) { | ||
warn("WATCH/UNWATCH not currently supported for http(2), falling-back to ws"); | ||
if (!this.ws) { | ||
const domain = this._domain.replace(/^https?:\/\//, ""); | ||
this.ws = new websocket_1.WebSocketClient(domain, this.concurrency); | ||
await this.ws.awaitConnection(); | ||
if (!__classPrivateFieldGet(this, _HttpClient_ws, "f")) { | ||
// Open a WebSocket connection | ||
const domain = __classPrivateFieldGet(this, _HttpClient_domain, "f").replace(/^https?:\/\//, ""); | ||
__classPrivateFieldSet(this, _HttpClient_ws, new websocket_1.WebSocketClient(domain, __classPrivateFieldGet(this, _HttpClient_concurrency, "f")), "f"); | ||
await __classPrivateFieldGet(this, _HttpClient_ws, "f").awaitConnection(); | ||
} | ||
return (_a = this.ws) === null || _a === void 0 ? void 0 : _a.request(req, callback, timeout); | ||
return __classPrivateFieldGet(this, _HttpClient_ws, "f").request(req, callback, timeout); | ||
} | ||
@@ -108,8 +145,10 @@ if (!req.requestId) | ||
trace("Adding http request w/ id %s to the queue", req.requestId); | ||
return this._q.add(() => errors_1.handleErrors(this.doRequest.bind(this), req, timeout)); | ||
return __classPrivateFieldGet(this, _HttpClient_q, "f").add(() => errors_1.handleErrors(this.doRequest.bind(this), req, timeout)); | ||
} | ||
/** send a request to server */ | ||
async doRequest(req, timeout) { | ||
// Send object to the server. | ||
trace("Pulled request %s from queue, starting on it", req.requestId); | ||
request_1.assert(req); | ||
trace("Req looks like socket request, awaiting race of timeout and fetch to %s%s", this._domain, req.path); | ||
trace("Req looks like socket request, awaiting race of timeout and fetch to %s%s", __classPrivateFieldGet(this, _HttpClient_domain, "f"), req.path); | ||
let timedout = false; | ||
@@ -125,8 +164,16 @@ let signal = undefined; | ||
} | ||
const result = await this.context | ||
.fetch(new URL(req.path, this._domain).toString(), { | ||
// Assume anything that is not a Buffer should be JSON? | ||
const body = Buffer.isBuffer(req.data) | ||
? req.data | ||
: JSON.stringify(req.data); | ||
const result = await __classPrivateFieldGet(this, _HttpClient_context, "f") | ||
.fetch(new URL(req.path, __classPrivateFieldGet(this, _HttpClient_domain, "f")).toString(), { | ||
// @ts-ignore | ||
method: req.method.toUpperCase(), | ||
// @ts-ignore | ||
signal, | ||
timeout, | ||
body: JSON.stringify(req.data), | ||
body, | ||
// We are not explicitly sending token in each request | ||
// because parent library sends it | ||
headers: req.headers, | ||
@@ -140,9 +187,12 @@ }) | ||
trace("Fetch did not throw, checking status of %s", result.status); | ||
if (result.status < 200 || result.status >= 300) { | ||
trace(`result.status (${result.status}) is not 2xx, throwing`); | ||
// This is the same test as in ./websocket.ts | ||
if (!result.ok) { | ||
trace("result.status %s is not 2xx, throwing", result.status); | ||
throw result; | ||
} | ||
trace("result.status ok, pulling headers"); | ||
// have to construct the headers ourselves: | ||
const headers = {}; | ||
if (Array.isArray(result.headers)) { | ||
// In browser they are an array? | ||
result.headers.forEach((value, key) => (headers[key] = value)); | ||
@@ -156,9 +206,10 @@ } | ||
const length = +(result.headers.get("content-length") || 0); | ||
let data = null; | ||
let data; | ||
if (req.method.toUpperCase() !== "HEAD") { | ||
const isJSON = (result.headers.get("content-type") || "").match(/json/); | ||
if (!isJSON) { | ||
data = await result.arrayBuffer(); | ||
if (!type_is_1.default.is(result.headers.get("content-type"), ["json", "+json"])) { | ||
data = Buffer.from(await result.arrayBuffer()); | ||
} | ||
else { | ||
// this json() function is really finicky, | ||
// have to do all these tests prior to get it to work | ||
data = await result.json(); | ||
@@ -178,2 +229,3 @@ } | ||
exports.HttpClient = HttpClient; | ||
_HttpClient_domain = new WeakMap(), _HttpClient_token = new WeakMap(), _HttpClient_status = new WeakMap(), _HttpClient_q = new WeakMap(), _HttpClient_initialConnection = new WeakMap(), _HttpClient_concurrency = new WeakMap(), _HttpClient_context = new WeakMap(), _HttpClient_ws = new WeakMap(); | ||
//# sourceMappingURL=http.js.map |
import { OADAClient, Config } from "./client"; | ||
/** Create a new instance of OADAClient */ | ||
export declare function createInstance(config: Config): OADAClient; | ||
/** @deprecated ws is deprecated, use http */ | ||
export declare function connect(config: Config & { | ||
connection: "ws"; | ||
}): Promise<OADAClient>; | ||
/** Create a new instance and wrap it with Promise */ | ||
export declare function connect(config: Config): Promise<OADAClient>; | ||
export { OADAClient, Config, GETRequest, PUTRequest, HEADRequest, WatchRequest, ConnectionRequest, ConnectionResponse, ConnectionChange, Connection, } from "./client"; | ||
export declare type Json = null | boolean | number | string | Json[] | { | ||
[prop: string]: Json; | ||
export declare type JsonPrimitive = string | number | boolean | null; | ||
export declare type JsonArray = Json[]; | ||
export declare type JsonObject = { | ||
[prop in string]?: Json; | ||
}; | ||
export declare type Json = JsonPrimitive | JsonObject | JsonArray; | ||
export declare type JsonCompatible<T> = { | ||
@@ -13,5 +22,7 @@ [P in keyof T]: T[P] extends Json ? T[P] : Pick<T, P> extends Required<Pick<T, P>> ? never : T[P] extends (() => unknown) | undefined ? never : JsonCompatible<T[P]>; | ||
type: "merge" | "delete"; | ||
body: Json; | ||
body: JsonObject & { | ||
_rev: number | string; | ||
}; | ||
path: string; | ||
resource_id: string; | ||
} |
@@ -5,2 +5,3 @@ "use strict"; | ||
const client_1 = require("./client"); | ||
/** Create a new instance of OADAClient */ | ||
function createInstance(config) { | ||
@@ -11,4 +12,7 @@ return new client_1.OADAClient(config); | ||
async function connect(config) { | ||
// Create an instance of client and start connection | ||
const client = new client_1.OADAClient(config); | ||
// Wait for the connection to open | ||
await client.awaitConnection(); | ||
// Return the instance | ||
return client; | ||
@@ -15,0 +19,0 @@ } |
@@ -0,7 +1,8 @@ | ||
import type { OADATree } from "./client"; | ||
export declare function toStringPath(path: Array<string>): string; | ||
export declare function toArrayPath(path: string): Array<string>; | ||
export declare function getObjectAtPath(tree: object, path: Array<string>): object; | ||
export declare function toTreePath(tree: object, path: Array<string>): Array<string>; | ||
export declare function isResource(tree: object, path: Array<string>): boolean; | ||
export declare function getObjectAtPath(tree: OADATree, path: Array<string>): OADATree; | ||
export declare function toTreePath(tree: OADATree, path: Array<string>): Array<string>; | ||
export declare function isResource(tree: OADATree, path: Array<string>): boolean; | ||
export declare function createNestedObject(obj: object, nestPath: Array<string>): object; | ||
export declare function delay(ms: number): Promise<unknown>; |
"use strict"; | ||
// Some useful functions | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -20,2 +21,3 @@ exports.delay = exports.createNestedObject = exports.isResource = exports.toTreePath = exports.getObjectAtPath = exports.toArrayPath = exports.toStringPath = void 0; | ||
function getObjectAtPath(tree, path) { | ||
// @ts-ignore | ||
return path.reduce((acc, nextKey) => { | ||
@@ -36,2 +38,3 @@ if (acc[nextKey]) { | ||
const treePath = []; | ||
// @ts-ignore | ||
path.reduce((acc, nextKey) => { | ||
@@ -66,8 +69,8 @@ if (acc[nextKey]) { | ||
return reversedArray.reduce((acc, nextKey) => { | ||
let newObj = {}; | ||
newObj[nextKey] = acc; | ||
return newObj; | ||
return { [nextKey]: acc }; | ||
}, obj); | ||
} | ||
exports.createNestedObject = createNestedObject; | ||
// Return delay promise | ||
// Reference: https://gist.github.com/joepie91/2664c85a744e6bd0629c#gistcomment-3082531 | ||
function delay(ms) { | ||
@@ -74,0 +77,0 @@ return new Promise((_) => setTimeout(_, ms)); |
@@ -5,14 +5,18 @@ /// <reference types="node" /> | ||
export declare class WebSocketClient extends EventEmitter implements Connection { | ||
private _ws; | ||
private _domain; | ||
private _status; | ||
private _requests; | ||
private _q; | ||
#private; | ||
/** | ||
* Constructor | ||
* @param domain Domain. E.g., www.example.com | ||
* @param concurrency Number of allowed in-flight requests. Default 10. | ||
*/ | ||
constructor(domain: string, concurrency?: number); | ||
/** Disconnect the WebSocket connection */ | ||
disconnect(): Promise<void>; | ||
/** Return true if connected, otherwise false */ | ||
isConnected(): boolean; | ||
/** Wait for the connection to open */ | ||
awaitConnection(): Promise<void>; | ||
request(req: ConnectionRequest, callback?: (response: Readonly<ConnectionChange>) => void, timeout?: number): Promise<ConnectionResponse>; | ||
/** send a request to server */ | ||
private doRequest; | ||
private _receive; | ||
} |
"use strict"; | ||
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
if (kind === "m") throw new TypeError("Private method is not writable"); | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); | ||
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; | ||
}; | ||
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); | ||
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); | ||
}; | ||
var __rest = (this && this.__rest) || function (s, e) { | ||
@@ -16,2 +27,3 @@ var t = {}; | ||
}; | ||
var _WebSocketClient_instances, _WebSocketClient_ws, _WebSocketClient_domain, _WebSocketClient_status, _WebSocketClient_requests, _WebSocketClient_q, _WebSocketClient_receive; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -27,3 +39,2 @@ exports.WebSocketClient = void 0; | ||
const error = debug_1.default("@oada/client:ws:error"); | ||
const errors_1 = require("./errors"); | ||
const request_1 = require("@oada/types/oada/websockets/request"); | ||
@@ -33,2 +44,8 @@ const response_1 = require("@oada/types/oada/websockets/response"); | ||
const v2_1 = require("@oada/types/oada/change/v2"); | ||
const errors_1 = require("./errors"); | ||
/** | ||
* Override defaults for ws in node | ||
* | ||
* @todo make sure this does not break in browser | ||
*/ | ||
class BetterWebSocket extends WebSocket { | ||
@@ -41,8 +58,21 @@ constructor(url, protocols = [], _a = {}) { | ||
class WebSocketClient extends events_1.EventEmitter { | ||
/** | ||
* Constructor | ||
* @param domain Domain. E.g., www.example.com | ||
* @param concurrency Number of allowed in-flight requests. Default 10. | ||
*/ | ||
constructor(domain, concurrency = 10) { | ||
super(); | ||
this._domain = domain; | ||
this._requests = new Map(); | ||
this._status = 1; | ||
const ws = new reconnecting_websocket_1.default("wss://" + this._domain, [], { | ||
_WebSocketClient_instances.add(this); | ||
_WebSocketClient_ws.set(this, void 0); | ||
_WebSocketClient_domain.set(this, void 0); | ||
_WebSocketClient_status.set(this, void 0); | ||
_WebSocketClient_requests.set(this, void 0); | ||
_WebSocketClient_q.set(this, void 0); | ||
__classPrivateFieldSet(this, _WebSocketClient_domain, domain, "f"); | ||
__classPrivateFieldSet(this, _WebSocketClient_requests, new Map(), "f"); | ||
__classPrivateFieldSet(this, _WebSocketClient_status, 1 /* Connecting */, "f"); | ||
// create websocket connection | ||
const ws = new reconnecting_websocket_1.default("wss://" + __classPrivateFieldGet(this, _WebSocketClient_domain, "f"), [], { | ||
// Not sure why it needs so long, but 30s is the ws timeout | ||
connectionTimeout: 30 * 1000, | ||
@@ -53,6 +83,7 @@ WebSocket: BetterWebSocket, | ||
const errP = events_1.once(ws, "error").then((err) => Promise.reject(err)); | ||
this._ws = Promise.race([openP, errP]); | ||
__classPrivateFieldSet(this, _WebSocketClient_ws, Promise.race([openP, errP]), "f"); | ||
// register handlers | ||
ws.onopen = () => { | ||
trace("Connection opened"); | ||
this._status = 2; | ||
__classPrivateFieldSet(this, _WebSocketClient_status, 2 /* Connected */, "f"); | ||
this.emit("open"); | ||
@@ -62,3 +93,3 @@ }; | ||
trace("Connection closed"); | ||
this._status = 0; | ||
__classPrivateFieldSet(this, _WebSocketClient_status, 0 /* Disconnected */, "f"); | ||
this.emit("close"); | ||
@@ -68,34 +99,46 @@ }; | ||
trace(err, "Connection error"); | ||
//this.#status = ConnectionStatus.Disconnected; | ||
//this.emit("error"); | ||
}; | ||
ws.onmessage = this._receive.bind(this); | ||
this._q = new p_queue_1.default({ concurrency }); | ||
this._q.on("active", () => { | ||
trace("WS Queue. Size: %d pending: %d", this._q.size, this._q.pending); | ||
ws.onmessage = __classPrivateFieldGet(this, _WebSocketClient_instances, "m", _WebSocketClient_receive).bind(this); // explicitly pass the instance | ||
__classPrivateFieldSet(this, _WebSocketClient_q, new p_queue_1.default({ concurrency }), "f"); | ||
__classPrivateFieldGet(this, _WebSocketClient_q, "f").on("active", () => { | ||
trace("WS Queue. Size: %d pending: %d", __classPrivateFieldGet(this, _WebSocketClient_q, "f").size, __classPrivateFieldGet(this, _WebSocketClient_q, "f").pending); | ||
}); | ||
} | ||
/** Disconnect the WebSocket connection */ | ||
async disconnect() { | ||
if (this._status == 0) { | ||
if (__classPrivateFieldGet(this, _WebSocketClient_status, "f") == 0 /* Disconnected */) { | ||
return; | ||
} | ||
(await this._ws).close(); | ||
(await __classPrivateFieldGet(this, _WebSocketClient_ws, "f")).close(); | ||
} | ||
/** Return true if connected, otherwise false */ | ||
isConnected() { | ||
return this._status == 2; | ||
return __classPrivateFieldGet(this, _WebSocketClient_status, "f") == 2 /* Connected */; | ||
} | ||
/** Wait for the connection to open */ | ||
async awaitConnection() { | ||
await this._ws; | ||
// Wait for _ws to resolve and return | ||
await __classPrivateFieldGet(this, _WebSocketClient_ws, "f"); | ||
} | ||
request(req, callback, timeout) { | ||
return this._q.add(() => errors_1.handleErrors(this.doRequest.bind(this), req, callback, timeout)); | ||
return __classPrivateFieldGet(this, _WebSocketClient_q, "f").add(() => errors_1.handleErrors(this.doRequest.bind(this), req, callback, timeout)); | ||
} | ||
/** send a request to server */ | ||
async doRequest(req, callback, timeout) { | ||
// Send object to the server. | ||
const requestId = req.requestId || ksuid_1.default.randomSync().string; | ||
req.requestId = requestId; | ||
request_1.assert(req); | ||
(await this._ws).send(JSON.stringify(req)); | ||
(await __classPrivateFieldGet(this, _WebSocketClient_ws, "f")).send(JSON.stringify(req)); | ||
// Promise for request | ||
const request_promise = new Promise((resolve, reject) => { | ||
this._requests.set(requestId, { | ||
// save request | ||
__classPrivateFieldGet(this, _WebSocketClient_requests, "f").set(requestId, { | ||
resolve, | ||
reject, | ||
settled: false, | ||
/* If this is a watch request, set "persistent" flag to true so | ||
this request will not get deleted after the first response */ | ||
persistent: callback ? true : false, | ||
@@ -106,10 +149,13 @@ callback, | ||
if (timeout && timeout > 0) { | ||
// If timeout is specified, create another promise and use Promise.race | ||
const timeout_promise = new Promise((_resolve, reject) => { | ||
setTimeout(() => { | ||
const request = this._requests.get(requestId); | ||
// If the original request is still pending, delete it. | ||
// This is necessary to kill "zombie" requests. | ||
const request = __classPrivateFieldGet(this, _WebSocketClient_requests, "f").get(requestId); | ||
if (request && !request.settled) { | ||
request.reject("Request timeout"); | ||
this._requests.delete(requestId); | ||
request.reject("Request timeout"); // reject request promise | ||
__classPrivateFieldGet(this, _WebSocketClient_requests, "f").delete(requestId); | ||
} | ||
reject("Request timeout"); | ||
reject("Request timeout"); // reject timeout promise | ||
}, timeout); | ||
@@ -120,57 +166,63 @@ }); | ||
else { | ||
// If timeout is not specified, simply return the request promise | ||
return request_promise; | ||
} | ||
} | ||
_receive(m) { | ||
try { | ||
const msg = JSON.parse(m.data.toString()); | ||
const requestIds = Array.isArray(msg.requestId) | ||
? msg.requestId | ||
: [msg.requestId]; | ||
for (const requestId of requestIds) { | ||
const request = this._requests.get(requestId); | ||
if (request) { | ||
if (response_1.is(msg)) { | ||
if (!request.persistent) { | ||
this._requests.delete(requestId); | ||
} | ||
exports.WebSocketClient = WebSocketClient; | ||
_WebSocketClient_ws = new WeakMap(), _WebSocketClient_domain = new WeakMap(), _WebSocketClient_status = new WeakMap(), _WebSocketClient_requests = new WeakMap(), _WebSocketClient_q = new WeakMap(), _WebSocketClient_instances = new WeakSet(), _WebSocketClient_receive = function _WebSocketClient_receive(m) { | ||
try { | ||
const msg = JSON.parse(m.data.toString()); | ||
const requestIds = Array.isArray(msg.requestId) | ||
? msg.requestId | ||
: [msg.requestId]; | ||
for (const requestId of requestIds) { | ||
// find original request | ||
const request = __classPrivateFieldGet(this, _WebSocketClient_requests, "f").get(requestId); | ||
if (request) { | ||
if (response_1.is(msg)) { | ||
if (!request.persistent) { | ||
__classPrivateFieldGet(this, _WebSocketClient_requests, "f").delete(requestId); | ||
} | ||
// if the request is not settled, resolve/reject the corresponding promise | ||
if (!request.settled) { | ||
request.settled = true; | ||
if (msg.status >= 200 && msg.status < 300) { | ||
request.resolve(msg); | ||
} | ||
if (!request.settled) { | ||
request.settled = true; | ||
if (msg.status >= 200 && msg.status < 300) { | ||
request.resolve(msg); | ||
} | ||
else if (msg.status) { | ||
request.reject(msg); | ||
} | ||
else { | ||
throw new Error("Request failed"); | ||
} | ||
else if (msg.status) { | ||
request.reject(msg); | ||
} | ||
else { | ||
throw new Error("Request failed"); | ||
} | ||
} | ||
else if (request.callback && change_1.is(msg)) { | ||
v2_1.assert(msg.change); | ||
const m = { | ||
requestId: [requestId], | ||
resourceId: msg.resourceId, | ||
path_leftover: msg.path_leftover, | ||
change: msg.change.map((_a) => { | ||
var { body } = _a, rest = __rest(_a, ["body"]); | ||
return Object.assign(Object.assign({}, rest), { body: body }); | ||
}), | ||
}; | ||
request.callback(m); | ||
} | ||
else { | ||
throw new Error("Invalid websocket payload received"); | ||
} | ||
} | ||
else if (request.callback && change_1.is(msg)) { | ||
v2_1.assert(msg.change); | ||
// TODO: Would be nice if @oad/types know "unkown" as Json | ||
const m = { | ||
requestId: [requestId], | ||
resourceId: msg.resourceId, | ||
path_leftover: msg.path_leftover, | ||
change: msg.change.map((_a) => { | ||
var { body } = _a, rest = __rest(_a, ["body"]); | ||
return Object.assign(Object.assign({}, rest), { body }); | ||
}), | ||
}; | ||
request.callback(m); | ||
} | ||
else { | ||
throw new Error("Invalid websocket payload received"); | ||
} | ||
} | ||
} | ||
catch (e) { | ||
error("[Websocket %s] Received invalid response. Ignoring.", this._domain); | ||
trace(e, "[Websocket %s] Received invalid response", this._domain); | ||
} | ||
} | ||
} | ||
exports.WebSocketClient = WebSocketClient; | ||
catch (e) { | ||
error("[Websocket %s] Received invalid response. Ignoring.", __classPrivateFieldGet(this, _WebSocketClient_domain, "f")); | ||
trace(e, "[Websocket %s] Received invalid response", __classPrivateFieldGet(this, _WebSocketClient_domain, "f")); | ||
// No point in throwing here; the promise cannot be resolved because the | ||
// requestId cannot be retrieved; throwing will just blow up client | ||
} | ||
}; | ||
//# sourceMappingURL=websocket.js.map |
{ | ||
"name": "@oada/client", | ||
"version": "2.5.1", | ||
"version": "2.6.0", | ||
"description": "A lightweight client tool to interact with an OADA-compliant server", | ||
@@ -29,2 +29,3 @@ "repository": "https://github.com/OADA/client", | ||
"@oada/types": "^1.5.1", | ||
"buffer": "^6.0.3", | ||
"cross-fetch": "^3.0.6", | ||
@@ -38,2 +39,3 @@ "debug": "^4.1.1", | ||
"reconnecting-websocket": "^4.4.0", | ||
"type-is": "^1.6.18", | ||
"ws": "^8.0.0" | ||
@@ -48,2 +50,3 @@ }, | ||
"@types/node": "^16.4.13", | ||
"@types/type-is": "^1", | ||
"@types/uuid": "^8.3.1", | ||
@@ -50,0 +53,0 @@ "@types/ws": "^7.4.7", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
95816
1386
12
17
+ Addedbuffer@^6.0.3
+ Addedtype-is@^1.6.18
+ Addedbase64-js@1.5.1(transitive)
+ Addedbuffer@6.0.3(transitive)
+ Addedieee754@1.2.1(transitive)
+ Addedmedia-typer@0.3.0(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedtype-is@1.6.18(transitive)