@holochain/client
Advanced tools
Comparing version 0.18.0-dev.13 to 0.18.0-rc.0
@@ -19,2 +19,3 @@ import { UnsubscribeFunction } from "emittery"; | ||
private readonly callZomeTransform; | ||
private readonly appAuthenticationToken; | ||
cachedAppInfo?: AppInfo | null; | ||
@@ -107,3 +108,2 @@ private readonly appInfoRequester; | ||
private static requester; | ||
private containsCell; | ||
} | ||
@@ -110,0 +110,0 @@ /** |
import Emittery from "emittery"; | ||
import { omit } from "lodash-es"; | ||
import { CellType } from "../admin/index.js"; | ||
import { CellType, } from "../admin/index.js"; | ||
import { catchError, DEFAULT_TIMEOUT, getBaseRoleNameFromCloneId, HolochainError, isCloneId, promiseTimeout, requesterTransformer, } from "../common.js"; | ||
@@ -25,2 +25,3 @@ import { getHostZomeCallSigner, getLauncherEnvironment, } from "../../environments/launcher.js"; | ||
callZomeTransform; | ||
appAuthenticationToken; | ||
cachedAppInfo; | ||
@@ -35,3 +36,3 @@ appInfoRequester; | ||
networkInfoRequester; | ||
constructor(client, appInfo, callZomeTransform, defaultTimeout) { | ||
constructor(client, appInfo, token, callZomeTransform, defaultTimeout) { | ||
this.client = client; | ||
@@ -42,2 +43,3 @@ this.myPubKey = appInfo.agent_pub_key; | ||
this.callZomeTransform = callZomeTransform ?? defaultCallZomeTransform; | ||
this.appAuthenticationToken = token; | ||
this.emitter = new Emittery(); | ||
@@ -83,12 +85,6 @@ this.cachedAppInfo = appInfo; | ||
const client = await WsClient.connect(options.url, options.wsClientOptions); | ||
if (env?.APP_INTERFACE_TOKEN) { | ||
// Note: This will only work for multiple connections if a single_use = false token is provided | ||
await client.authenticate({ token: env.APP_INTERFACE_TOKEN }); | ||
} | ||
else { | ||
if (!options.token) { | ||
throw new HolochainError("AppAuthenticationTokenMissing", `unable to connect to Conductor API - no app authentication token provided.`); | ||
} | ||
await client.authenticate({ token: options.token }); | ||
} | ||
const token = options.token ?? env?.APP_INTERFACE_TOKEN; | ||
if (!token) | ||
throw new HolochainError("AppAuthenticationTokenMissing", `unable to connect to Conductor API - no app authentication token provided.`); | ||
await client.authenticate({ token }); | ||
const appInfo = await AppWebsocket.requester(client, "app_info", DEFAULT_TIMEOUT)(null); | ||
@@ -98,3 +94,3 @@ if (!appInfo) { | ||
} | ||
return new AppWebsocket(client, appInfo, options.callZomeTransform, options.defaultTimeout); | ||
return new AppWebsocket(client, appInfo, token, options.callZomeTransform, options.defaultTimeout); | ||
} | ||
@@ -245,21 +241,2 @@ /** | ||
} | ||
containsCell(cellId) { | ||
const appInfo = this.cachedAppInfo; | ||
if (!appInfo) { | ||
return false; | ||
} | ||
for (const roleName of Object.keys(appInfo.cell_info)) { | ||
for (const cellInfo of appInfo.cell_info[roleName]) { | ||
const currentCellId = CellType.Provisioned in cellInfo | ||
? cellInfo[CellType.Provisioned].cell_id | ||
: CellType.Cloned in cellInfo | ||
? cellInfo[CellType.Cloned].cell_id | ||
: undefined; | ||
if (currentCellId && isSameCell(currentCellId, cellId)) { | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
} | ||
@@ -266,0 +243,0 @@ const defaultCallZomeTransform = { |
@@ -26,4 +26,4 @@ /// <reference types="ws" /> | ||
private index; | ||
private authenticationToken; | ||
constructor(socket: IsoWebSocket, url?: URL, options?: WsClientOptions); | ||
private setupSocket; | ||
/** | ||
@@ -52,2 +52,6 @@ * Instance factory for creating WsClients. | ||
/** | ||
* Close the websocket connection. | ||
*/ | ||
close(code?: number): Promise<IsoWebSocket.CloseEvent>; | ||
/** | ||
* Send requests to the connected websocket. | ||
@@ -61,8 +65,7 @@ * | ||
private sendMessage; | ||
private registerMessageListener; | ||
private registerCloseListener; | ||
private reconnectWebsocket; | ||
private handleResponse; | ||
/** | ||
* Close the websocket connection. | ||
*/ | ||
close(code?: number): Promise<CloseEvent>; | ||
} | ||
export { IsoWebSocket }; |
@@ -20,4 +20,7 @@ import { decode, encode } from "@msgpack/msgpack"; | ||
index; | ||
authenticationToken; | ||
constructor(socket, url, options) { | ||
super(); | ||
this.registerMessageListener(socket); | ||
this.registerCloseListener(socket); | ||
this.socket = socket; | ||
@@ -28,64 +31,3 @@ this.url = url; | ||
this.index = 0; | ||
this.setupSocket(); | ||
} | ||
setupSocket() { | ||
this.socket.onmessage = async (serializedMessage) => { | ||
// If data is not a buffer (nodejs), it will be a blob (browser) | ||
let deserializedData; | ||
if (globalThis.window && | ||
serializedMessage.data instanceof globalThis.window.Blob) { | ||
deserializedData = await serializedMessage.data.arrayBuffer(); | ||
} | ||
else { | ||
if (typeof Buffer !== "undefined" && | ||
Buffer.isBuffer(serializedMessage.data)) { | ||
deserializedData = serializedMessage.data; | ||
} | ||
else { | ||
throw new HolochainError("UnknownMessageFormat", `incoming message has unknown message format - ${deserializedData}`); | ||
} | ||
} | ||
const message = decode(deserializedData); | ||
assertHolochainMessage(message); | ||
if (message.type === "signal") { | ||
if (message.data === null) { | ||
throw new HolochainError("UnknownSignalFormat", "incoming signal has no data"); | ||
} | ||
const deserializedSignal = decode(message.data); | ||
assertHolochainSignal(deserializedSignal); | ||
if (SignalType.System in deserializedSignal) { | ||
this.emit("signal", { | ||
System: deserializedSignal[SignalType.System], | ||
}); | ||
} | ||
else { | ||
const encodedAppSignal = deserializedSignal[SignalType.App]; | ||
// In order to return readable content to the UI, the signal payload must also be deserialized. | ||
const payload = decode(encodedAppSignal.signal); | ||
const signal = { | ||
cell_id: encodedAppSignal.cell_id, | ||
zome_name: encodedAppSignal.zome_name, | ||
payload, | ||
}; | ||
this.emit("signal", { App: signal }); | ||
} | ||
} | ||
else if (message.type === "response") { | ||
this.handleResponse(message); | ||
} | ||
else { | ||
throw new HolochainError("UnknownMessageType", `incoming message has unknown type - ${message.type}`); | ||
} | ||
}; | ||
this.socket.onclose = (event) => { | ||
const pendingRequestIds = Object.keys(this.pendingRequests).map((id) => parseInt(id)); | ||
if (pendingRequestIds.length) { | ||
pendingRequestIds.forEach((id) => { | ||
const error = new HolochainError("ClientClosedWithPendingRequests", `client closed with pending requests - close event code: ${event.code}, request id: ${id}`); | ||
this.pendingRequests[id].reject(error); | ||
delete this.pendingRequests[id]; | ||
}); | ||
} | ||
}; | ||
} | ||
/** | ||
@@ -101,9 +43,9 @@ * Instance factory for creating WsClients. | ||
const socket = new IsoWebSocket(url, options); | ||
socket.onerror = (errorEvent) => { | ||
socket.addEventListener("error", (errorEvent) => { | ||
reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - ${errorEvent.error}`)); | ||
}; | ||
socket.onopen = () => { | ||
}); | ||
socket.addEventListener("open", (_) => { | ||
const client = new WsClient(socket, url, options); | ||
resolve(client); | ||
}; | ||
}, { once: true }); | ||
}); | ||
@@ -131,3 +73,11 @@ } | ||
async authenticate(request) { | ||
return this.exchange(request, (request, resolve) => { | ||
this.authenticationToken = request.token; | ||
return this.exchange(request, (request, resolve, reject) => { | ||
const invalidTokenCloseListener = (closeEvent) => { | ||
this.authenticationToken = undefined; | ||
reject(new HolochainError("InvalidTokenError", `could not connect to ${this.url} due to an invalid app authentication token - close code ${closeEvent.code}`)); | ||
}; | ||
this.socket.addEventListener("close", invalidTokenCloseListener, { | ||
once: true, | ||
}); | ||
const encodedMsg = encode({ | ||
@@ -138,7 +88,18 @@ type: "authenticate", | ||
this.socket.send(encodedMsg); | ||
// Message just needs to be sent first, no need to wait for a response or even require a flush | ||
resolve(null); | ||
// Wait before resolving in case authentication fails. | ||
setTimeout(() => { | ||
this.socket.removeEventListener("close", invalidTokenCloseListener); | ||
resolve(null); | ||
}, 10); | ||
}); | ||
} | ||
/** | ||
* Close the websocket connection. | ||
*/ | ||
close(code = 1000) { | ||
const closedPromise = new Promise((resolve) => this.socket.addEventListener("close", (closeEvent) => resolve(closeEvent), { once: true })); | ||
this.socket.close(code); | ||
return closedPromise; | ||
} | ||
/** | ||
* Send requests to the connected websocket. | ||
@@ -152,3 +113,3 @@ * | ||
} | ||
exchange(request, sendHandler) { | ||
async exchange(request, sendHandler) { | ||
if (this.socket.readyState === this.socket.OPEN) { | ||
@@ -160,4 +121,11 @@ const promise = new Promise((resolve, reject) => { | ||
} | ||
else if (this.url && this.authenticationToken) { | ||
await this.reconnectWebsocket(this.url, this.authenticationToken); | ||
this.registerMessageListener(this.socket); | ||
this.registerCloseListener(this.socket); | ||
const promise = new Promise((resolve, reject) => sendHandler(request, resolve, reject)); | ||
return promise; | ||
} | ||
else { | ||
return Promise.reject(new Error("Socket is not open")); | ||
return Promise.reject(new HolochainError("WebsocketClosedError", "Websocket is not open")); | ||
} | ||
@@ -176,2 +144,93 @@ } | ||
} | ||
registerMessageListener(socket) { | ||
socket.onmessage = async (serializedMessage) => { | ||
// If data is not a buffer (nodejs), it will be a blob (browser) | ||
let deserializedData; | ||
if (globalThis.window && | ||
serializedMessage.data instanceof globalThis.window.Blob) { | ||
deserializedData = await serializedMessage.data.arrayBuffer(); | ||
} | ||
else { | ||
if (typeof Buffer !== "undefined" && | ||
Buffer.isBuffer(serializedMessage.data)) { | ||
deserializedData = serializedMessage.data; | ||
} | ||
else { | ||
throw new HolochainError("UnknownMessageFormat", `incoming message has unknown message format - ${deserializedData}`); | ||
} | ||
} | ||
const message = decode(deserializedData); | ||
assertHolochainMessage(message); | ||
if (message.type === "signal") { | ||
if (message.data === null) { | ||
throw new HolochainError("UnknownSignalFormat", "incoming signal has no data"); | ||
} | ||
const deserializedSignal = decode(message.data); | ||
assertHolochainSignal(deserializedSignal); | ||
if (SignalType.System in deserializedSignal) { | ||
this.emit("signal", { | ||
System: deserializedSignal[SignalType.System], | ||
}); | ||
} | ||
else { | ||
const encodedAppSignal = deserializedSignal[SignalType.App]; | ||
// In order to return readable content to the UI, the signal payload must also be deserialized. | ||
const payload = decode(encodedAppSignal.signal); | ||
const signal = { | ||
cell_id: encodedAppSignal.cell_id, | ||
zome_name: encodedAppSignal.zome_name, | ||
payload, | ||
}; | ||
this.emit("signal", { App: signal }); | ||
} | ||
} | ||
else if (message.type === "response") { | ||
this.handleResponse(message); | ||
} | ||
else { | ||
throw new HolochainError("UnknownMessageType", `incoming message has unknown type - ${message.type}`); | ||
} | ||
}; | ||
} | ||
registerCloseListener(socket) { | ||
socket.addEventListener("close", (closeEvent) => { | ||
const pendingRequestIds = Object.keys(this.pendingRequests).map((id) => parseInt(id)); | ||
if (pendingRequestIds.length) { | ||
pendingRequestIds.forEach((id) => { | ||
const error = new HolochainError("ClientClosedWithPendingRequests", `client closed with pending requests - close event code: ${closeEvent.code}, request id: ${id}`); | ||
this.pendingRequests[id].reject(error); | ||
delete this.pendingRequests[id]; | ||
}); | ||
} | ||
}, { once: true }); | ||
} | ||
async reconnectWebsocket(url, token) { | ||
return new Promise((resolve, reject) => { | ||
this.socket = new IsoWebSocket(url, this.options); | ||
// This error event never occurs in tests. Could be removed? | ||
this.socket.addEventListener("error", (errorEvent) => { | ||
this.authenticationToken = undefined; | ||
reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - ${errorEvent.message}`)); | ||
}, { once: true }); | ||
const invalidTokenCloseListener = (closeEvent) => { | ||
this.authenticationToken = undefined; | ||
reject(new HolochainError("InvalidTokenError", `could not connect to ${this.url} due to an invalid app authentication token - close code ${closeEvent.code}`)); | ||
}; | ||
this.socket.addEventListener("close", invalidTokenCloseListener, { | ||
once: true, | ||
}); | ||
this.socket.addEventListener("open", async (_) => { | ||
const encodedMsg = encode({ | ||
type: "authenticate", | ||
data: encode({ token }), | ||
}); | ||
this.socket.send(encodedMsg); | ||
// Wait in case authentication fails. | ||
setTimeout(() => { | ||
this.socket.removeEventListener("close", invalidTokenCloseListener); | ||
resolve(); | ||
}, 10); | ||
}, { once: true }); | ||
}); | ||
} | ||
handleResponse(msg) { | ||
@@ -192,16 +251,2 @@ const id = msg.id; | ||
} | ||
/** | ||
* Close the websocket connection. | ||
*/ | ||
close(code = 1000) { | ||
const closedPromise = new Promise((resolve) => | ||
// for an unknown reason "addEventListener" is seen as a non-callable | ||
// property and gives a ts2349 error | ||
// type assertion as workaround | ||
this.socket.addEventListener("close", (event) => resolve(event)) | ||
// } | ||
); | ||
this.socket.close(code); | ||
return closedPromise; | ||
} | ||
} | ||
@@ -208,0 +253,0 @@ function assertHolochainMessage(message) { |
@@ -55,2 +55,3 @@ const ERROR_TYPE = "error"; | ||
.catch((e) => { | ||
clearTimeout(id); | ||
return rej(e); | ||
@@ -57,0 +58,0 @@ })); |
@@ -8,5 +8,5 @@ // This file is read by tools that parse documentation comments conforming to the TSDoc standard. | ||
"packageName": "@microsoft/api-extractor", | ||
"packageVersion": "7.47.0" | ||
"packageVersion": "7.47.11" | ||
} | ||
] | ||
} |
{ | ||
"name": "@holochain/client", | ||
"version": "0.18.0-dev.13", | ||
"version": "0.18.0-rc.0", | ||
"description": "A JavaScript client for the Holochain Conductor API", | ||
@@ -75,2 +75,2 @@ "author": "Holochain Foundation <info@holochain.org> (https://holochain.org)", | ||
} | ||
} | ||
} |
@@ -182,4 +182,5 @@ [![Project](https://img.shields.io/badge/Project-Holochain-blue.svg?style=flat-square)](http://holochain.org/) | ||
```bash | ||
nix-shell | ||
./run-test.sh | ||
nix develop | ||
./build-fixture.sh | ||
npm run test | ||
``` | ||
@@ -186,0 +187,0 @@ |
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
136969
3773
203