ic-websocket-js
Version:
IC WebSocket on the Internet Computer
157 lines • 6.67 kB
JavaScript
import { Principal } from '@dfinity/principal';
import { IdentityInvalidError, makeNonce, Cbor, Expiry, SubmitRequestType, CanisterStatus, } from '@dfinity/agent';
import { makeWsNonceTransform } from './transforms';
// Default delta for ingress expiry is 5 minutes.
const DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS = 5 * 60 * 1000;
class DefaultWsError extends Error {
constructor(message) {
super(message);
this.message = message;
Object.setPrototypeOf(this, DefaultWsError.prototype);
}
}
export class WsAgent {
constructor(options) {
this._pipeline = [];
this._timeDiffMsecs = 0;
this._isAgent = true;
if (options.source) {
if (!(options.source instanceof WsAgent)) {
throw new Error("An Agent's source can only be another WsAgent");
}
this._pipeline = [...options.source._pipeline];
this._identity = options.source._identity;
this._ws = options.source._ws;
this._httpAgent = options.source._httpAgent;
}
else {
if (!options.identity) {
throw new Error('An identity must be provided to the WsAgent');
}
this._identity = Promise.resolve(options.identity);
if (!options.ws) {
throw new Error('A WebSocket instance must be provided to the WsAgent');
}
else if (options.ws.readyState !== WebSocket.OPEN) {
throw new DefaultWsError('The provided WebSocket is not open');
}
this._ws = options.ws;
if (!options.httpAgent) {
throw new Error('An httpAgent must be provided to the WsAgent');
}
this._httpAgent = options.httpAgent;
}
// Default is 3, only set from option if greater or equal to 0
this._retryTimes =
options.retryTimes !== undefined && options.retryTimes >= 0 ? options.retryTimes : 3;
// Add a nonce transform to ensure calls are unique
if (!options.disableNonce) {
this.addTransform(makeWsNonceTransform(makeNonce));
}
}
addTransform(fn, priority = fn.priority || 0) {
// Keep the pipeline sorted at all time, by priority.
const i = this._pipeline.findIndex(x => (x.priority || 0) < priority);
this._pipeline.splice(i >= 0 ? i : this._pipeline.length, 0, Object.assign(fn, { priority }));
}
async getPrincipal() {
if (!this._identity) {
throw new IdentityInvalidError("This identity has expired due this application's security policy. Please refresh your authentication.");
}
return (await this._identity).getPrincipal();
}
async call(canisterId, options) {
const id = await this._identity;
if (!id) {
throw new IdentityInvalidError("This identity has expired due this application's security policy. Please refresh your authentication.");
}
const canister = Principal.from(canisterId);
const sender = id.getPrincipal();
let ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS);
// If the value is off by more than 30 seconds, reconcile system time with the network
if (Math.abs(this._timeDiffMsecs) > 1000 * 30) {
ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS + this._timeDiffMsecs);
}
const submit = {
request_type: SubmitRequestType.Call,
canister_id: canister,
method_name: options.methodName,
arg: options.arg,
sender,
ingress_expiry,
};
const transformedRequest = (await this._transform({
endpoint: "call" /* Endpoint.Call */,
message: submit,
}));
// we need to adapt out ws request to the identity's transform
// in order to get the signed request
const idTransformedRequest = await id.transformRequest({
// mock the request to be compatible
request: {
body: null,
method: 'POST',
headers: {},
},
endpoint: "call" /* Endpoint.Call */,
body: transformedRequest.message,
});
const message = {
envelope: idTransformedRequest.body,
};
this._requestAndRetry(message);
}
/**
* Sends a fire-and-forget request body to the WebSocket Gateway.
* If the request fails, the request
*/
_requestAndRetry(message, tries = 0) {
const messageBytes = Cbor.encode(message);
try {
return this._ws.send(messageBytes);
}
catch (error) {
if (this._retryTimes > tries) {
console.warn(`${error} Retrying request.`);
return this._requestAndRetry(message, tries + 1);
}
}
throw new DefaultWsError("Sending the envelope through the WebSocket failed.");
}
/**
* Allows agent to sync its time with the network. Can be called during intialization or mid-lifecycle if the device's clock has drifted away from the network time. This is necessary to set the Expiry for a request
* @param {Principal} canisterId - Pass a canister ID if you need to sync the time with a particular replica. Uses the management canister by default
*/
async syncTime(canisterId) {
const callTime = Date.now();
try {
if (!canisterId) {
console.log('Syncing time with the IC. No canisterId provided, so falling back to ryjl3-tyaaa-aaaaa-aaaba-cai');
}
const status = await CanisterStatus.request({
// Fall back with canisterId of the ICP Ledger
canisterId: canisterId !== null && canisterId !== void 0 ? canisterId : Principal.from('ryjl3-tyaaa-aaaaa-aaaba-cai'),
agent: this._httpAgent,
paths: ['time'],
});
const replicaTime = status.get('time');
if (replicaTime) {
this._timeDiffMsecs = Number(replicaTime) - Number(callTime);
}
}
catch (error) {
console.error('Caught exception while attempting to sync time:', error);
}
}
replaceIdentity(identity) {
this._identity = Promise.resolve(identity);
}
_transform(request) {
let p = Promise.resolve(request);
for (const fn of this._pipeline) {
p = p.then(r => fn(r).then(r2 => r2 || r));
}
return p;
}
}
//# sourceMappingURL=index.js.map