@daml/ledger
Version:
Client side API implementation for a Daml based ledger. This library implements the JSON based API for a Daml ledger documented in https://docs.daml.com/json-api/index.html.
557 lines • 26.9 kB
JavaScript
;
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var types_1 = require("@daml/types");
var index_1 = __importDefault(require("./index"));
var index_2 = require("./index");
var jtv = __importStar(require("@mojotech/json-type-validation"));
var jest_mock_console_1 = __importDefault(require("jest-mock-console"));
var mockLive = jest.fn();
var mockChange = jest.fn();
var mockConstructor = jest.fn();
var mockSend = jest.fn();
var mockClose = jest.fn();
var mockFunctions = [
mockLive,
mockChange,
mockConstructor,
mockSend,
mockClose,
];
var fooKey = "fooKey";
var mockInstance = undefined;
jest.mock("isomorphic-ws", function () {
return /** @class */ (function () {
function class_1() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
mockConstructor.apply(void 0, args);
// eslint-disable-next-line @typescript-eslint/no-this-alias
mockInstance = this;
// eslint-disable-next-line @typescript-eslint/no-var-requires
var EventEmitter = require("events").EventEmitter;
this.eventEmitter = new EventEmitter();
// eslint-disable-next-line @typescript-eslint/no-var-requires
var WsState = require("./index").WsState;
this.readyState = WsState.Connecting;
}
class_1.prototype.addEventListener = function (event, handler) {
this.eventEmitter.on(event, handler);
};
class_1.prototype.send = function (message) {
mockSend(JSON.parse(message));
};
class_1.prototype.close = function () {
// eslint-disable-next-line @typescript-eslint/no-var-requires
var WsState = require("./index").WsState;
this.readyState = WsState.Closed;
mockClose();
};
class_1.prototype.serverOpen = function () {
this.eventEmitter.emit("open");
// eslint-disable-next-line @typescript-eslint/no-var-requires
var WsState = require("./index").WsState;
this.readyState = WsState.Open;
};
class_1.prototype.serverSend = function (message) {
this.eventEmitter.emit("message", { data: JSON.stringify(message) });
};
class_1.prototype.serverClose = function (event) {
this.eventEmitter.emit("close", event);
// eslint-disable-next-line @typescript-eslint/no-var-requires
var WsState = require("./index").WsState;
this.readyState = WsState.Closing;
};
return class_1;
}());
});
var Foo = {
sdkVersion: "2.10.2",
templateId: "foo-id",
keyDecoder: jtv.string(),
keyEncode: function (s) { return s; },
decoder: jtv.object({ key: jtv.string() }),
encode: function (o) { return o; },
// eslint-disable-next-line @typescript-eslint/ban-types
Archive: {},
};
var fooCreateEvent = function (coid, key) {
return {
templateId: "foo-id",
contractId: coid.toString(),
signatories: [],
observers: [],
agreementText: "fooAgreement",
key: key || fooKey,
payload: { key: fooKey },
};
};
var fooEvent = function (coid) {
return { created: fooCreateEvent(coid), matchedQueries: [0] };
};
var fooArchiveEvent = function (coid) {
return {
archived: {
templateId: "foo-id",
contractId: coid.toString(),
},
};
};
var mockOptions = {
token: "dummyToken",
httpBaseUrl: "http://localhost:5000/",
wsBaseUrl: "ws://localhost:4000/",
};
beforeEach(function () {
mockFunctions.forEach(function (f) { return f.mockClear(); });
});
describe("internals", function () {
test("assert throws as expected", function () {
(0, index_2.assert)(true, "not thrown");
expect(function () { return (0, index_2.assert)(false, "throws"); }).toThrow();
});
});
describe("streamSubmit", function () {
test("receive unknown message", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQuery(Foo);
expect(mockConstructor).toHaveBeenCalledTimes(1);
expect(mockConstructor).toHaveBeenLastCalledWith("ws://localhost:4000/v1/stream/query", ["jwt.token.dummyToken", "daml.ws.auth"]);
stream.on("change", mockChange);
mockInstance.serverOpen();
expect(mockSend).toHaveBeenCalledTimes(1);
expect(mockSend).toHaveBeenLastCalledWith([
{ templateIds: [Foo.templateId] },
]);
var restoreConsole = (0, jest_mock_console_1.default)();
mockInstance.serverSend("mickey mouse");
expect(console.error).toHaveBeenCalledWith("Ledger.streamQuery unknown message", "mickey mouse");
restoreConsole();
});
test("receive warnings", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQueries(Foo, []);
stream.on("change", mockChange);
var restoreConsole = (0, jest_mock_console_1.default)();
mockInstance.serverOpen();
mockInstance.serverSend({ warnings: ["oh oh"] });
expect(console.warn).toHaveBeenCalledWith("Ledger.streamQueries warnings", {
warnings: ["oh oh"],
});
restoreConsole();
});
test("receive errors", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKey(Foo, fooKey);
stream.on("change", mockChange);
var restoreConsole = (0, jest_mock_console_1.default)();
mockInstance.serverOpen();
mockInstance.serverSend({ errors: ["not good!"] });
expect(console.error).toHaveBeenCalledWith("Ledger.streamFetchByKey errors", { errors: ["not good!"] });
restoreConsole();
});
test("receive null offset", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQuery(Foo);
stream.on("live", mockLive);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverOpen();
mockInstance.serverSend({ events: [], offset: null });
expect(mockLive).toHaveBeenCalledTimes(1);
expect(mockLive).toHaveBeenLastCalledWith([]);
expect(mockChange).not.toHaveBeenCalled();
});
test("reconnect on server close with appropriate offsets, when ws multiplexing is enabled", function () { return __awaiter(void 0, void 0, void 0, function () {
var reconnectThreshold, ledger, stream;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
reconnectThreshold = 200;
ledger = new index_1.default(__assign(__assign({}, mockOptions), { reconnectThreshold: reconnectThreshold, multiplexQueryStreams: true }));
stream = ledger.streamQuery(Foo, { key: "1" });
stream.on("live", mockLive);
stream.on("close", mockClose);
mockInstance.serverOpen();
mockInstance.serverSend({ events: [], offset: "4" });
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, reconnectThreshold * 2); })];
case 1:
_a.sent();
mockConstructor.mockClear();
mockInstance.serverClose({ code: 1, reason: "test close" });
expect(mockConstructor).toHaveBeenCalled();
mockInstance.serverOpen();
//first query with no offsets.
expect(mockSend).toHaveBeenNthCalledWith(1, [
{ query: { key: "1" }, templateIds: ["foo-id"] },
]);
//subsequent one on reconnection with offsets received
expect(mockSend).toHaveBeenNthCalledWith(2, [
{ offset: "4", query: { key: "1" }, templateIds: ["foo-id"] },
]);
mockSend.mockClear();
mockConstructor.mockClear();
// check that the client doesn't try to reconnect again. it should only reconnect if it
// received an event confirming the stream is live again, i.e. {events: [], offset: '4'}
mockInstance.serverClose({ code: 1, reason: "test close" });
expect(mockConstructor).not.toHaveBeenCalled();
return [2 /*return*/];
}
});
}); });
test("reconnect on server close with appropriate offsets, when ws multiplexing is disabled", function () { return __awaiter(void 0, void 0, void 0, function () {
var reconnectThreshold, ledger, stream;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
reconnectThreshold = 200;
ledger = new index_1.default(__assign(__assign({}, mockOptions), { reconnectThreshold: reconnectThreshold, multiplexQueryStreams: false }));
stream = ledger.streamQuery(Foo);
stream.on("live", mockLive);
stream.on("close", mockClose);
mockInstance.serverOpen();
mockInstance.serverSend({ events: [], offset: "3" });
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, reconnectThreshold * 2); })];
case 1:
_a.sent();
mockConstructor.mockClear();
mockInstance.serverClose({ code: 1, reason: "test close" });
expect(mockConstructor).toHaveBeenCalled();
mockInstance.serverOpen();
expect(mockSend).toHaveBeenNthCalledWith(1, [{ templateIds: ["foo-id"] }]); //initial query
expect(mockSend).toHaveBeenNthCalledWith(2, { offset: "3" }); // offsets sent when reconnecting.
expect(mockSend).toHaveBeenNthCalledWith(3, [{ templateIds: ["foo-id"] }]); //reconnect query request.
mockSend.mockClear();
mockConstructor.mockClear();
// check that the client doesn't try to reconnect again. it should only reconnect if it
// received an event confirming the stream is live again, i.e. {events: [], offset: '4'}
mockInstance.serverClose({ code: 1, reason: "test close" });
expect(mockConstructor).not.toHaveBeenCalled();
return [2 /*return*/];
}
});
}); });
test("do not reconnect on client close", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQuery(Foo);
expect(mockConstructor).toHaveBeenCalled();
mockConstructor.mockClear();
stream.close();
expect(mockConstructor).not.toHaveBeenCalled();
});
test("receive empty events", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQuery(Foo);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverSend({ events: [] });
expect(mockChange).toHaveBeenCalledTimes(0);
});
test("stop listening to a stream", function () {
var ledger = new index_1.default(mockOptions);
(0, types_1.registerTemplate)(Foo);
var stream = ledger.streamQuery(Foo);
var count1 = jest.fn();
var count2 = jest.fn();
stream.on("change", count1);
stream.on("change", count2);
mockInstance.serverOpen();
mockInstance.serverSend({ events: [1, 2, 3].map(fooEvent) });
expect(count1).toHaveBeenCalledTimes(1);
expect(count2).toHaveBeenCalledTimes(1);
stream.off("change", count1);
mockInstance.serverSend({ events: [1, 2, 3].map(fooEvent) });
expect(count1).toHaveBeenCalledTimes(1);
expect(count2).toHaveBeenCalledTimes(2);
});
});
describe("streamQuery", function () {
test("receive live event", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQuery(Foo);
stream.on("live", mockLive);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverOpen();
mockInstance.serverSend({ events: [fooEvent(1)], offset: "3" });
expect(mockLive).toHaveBeenCalledTimes(1);
expect(mockLive).toHaveBeenLastCalledWith([fooCreateEvent(1)]);
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenLastCalledWith([fooCreateEvent(1)]);
});
test("receive one event", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQuery(Foo);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverOpen();
mockInstance.serverSend({ events: [fooEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenLastCalledWith([fooCreateEvent(1)]);
});
test("receive several events", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQuery(Foo);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverOpen();
mockInstance.serverSend({ events: [1, 2, 3].map(fooEvent) });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([1, 2, 3].map(function (cid) { return fooCreateEvent(cid); }));
});
test("drop matching created and archived events", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQuery(Foo);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverOpen();
mockInstance.serverSend({ events: [fooEvent(1), fooEvent(2)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([
fooCreateEvent(1),
fooCreateEvent(2),
]);
mockChange.mockClear();
mockInstance.serverSend({ events: [fooArchiveEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([fooCreateEvent(2)]);
});
});
describe("streamQueries", function () {
test("receive live event", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQueries(Foo, []);
stream.on("live", mockLive);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverOpen();
mockInstance.serverSend({ events: [fooEvent(1)], offset: "3" });
expect(mockLive).toHaveBeenCalledTimes(1);
expect(mockLive).toHaveBeenLastCalledWith([fooCreateEvent(1)]);
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenLastCalledWith([fooCreateEvent(1)]);
});
test("receive one event", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQueries(Foo, []);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverOpen();
mockInstance.serverSend({ events: [fooEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenLastCalledWith([fooCreateEvent(1)]);
});
test("receive several events", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQueries(Foo, []);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverOpen();
mockInstance.serverSend({ events: [1, 2, 3].map(fooEvent) });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([1, 2, 3].map(function (cid) { return fooCreateEvent(cid); }));
});
test("drop matching created and archived events", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamQueries(Foo, []);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverOpen();
mockInstance.serverSend({ events: [fooEvent(1), fooEvent(2)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([
fooCreateEvent(1),
fooCreateEvent(2),
]);
mockChange.mockClear();
mockInstance.serverSend({ events: [fooArchiveEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([fooCreateEvent(2)]);
});
});
describe("streamFetchByKey", function () {
test("receive no event", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKey(Foo, "badKey");
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverSend({ events: [] });
expect(mockChange).toHaveBeenCalledTimes(0);
});
test("receive one event", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKey(Foo, fooKey);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverSend({ events: [fooEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith(fooCreateEvent(1));
});
test("receive several events", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKey(Foo, fooKey);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverSend({
events: [fooEvent(1), fooEvent(2), fooEvent(3)],
});
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith(fooCreateEvent(3));
});
test("drop matching created and archived events", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKey(Foo, fooKey);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverSend({ events: [fooEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith(fooCreateEvent(1));
mockChange.mockClear();
mockInstance.serverSend({ events: [fooArchiveEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith(null);
});
});
describe("streamFetchByKeys", function () {
test("receive one event", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKeys(Foo, [fooKey]);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverSend({ events: [fooEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([fooCreateEvent(1)]);
});
test("receive several events", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKeys(Foo, [fooKey]);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverSend({
events: [fooEvent(1), fooEvent(2), fooEvent(3)],
});
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([fooCreateEvent(3)]);
});
test("drop matching created and archived events", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKeys(Foo, [fooKey]);
stream.on("change", function (state) { return mockChange(state); });
mockInstance.serverSend({ events: [fooEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([fooCreateEvent(1)]);
mockChange.mockClear();
mockInstance.serverSend({ events: [fooArchiveEvent(1)] });
expect(mockChange).toHaveBeenCalledTimes(1);
expect(mockChange).toHaveBeenCalledWith([null]);
});
test("watch multiple keys", function () {
var create = function (cid, key) { return ({
created: fooCreateEvent(cid, key),
matchedQueries: [0],
}); };
var archive = fooArchiveEvent;
var send = function (events) {
return mockInstance.serverSend({ events: events });
};
var expectCids = function (expected) {
return expect(mockChange).toHaveBeenCalledWith(expected.map(function (cid, idx) {
return cid ? fooCreateEvent(cid, "key".concat(idx + 1)) : null;
}));
};
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKeys(Foo, ["key1", "key2"]);
stream.on("change", function (state) { return mockChange(state); });
send([create(1, "key1")]);
expectCids([1, null]);
send([create(2, "key2")]);
expectCids([1, 2]);
send([archive(1), create(3, "key1")]);
expectCids([3, 2]);
send([archive(2), archive(3), create(4, "key2")]);
expectCids([null, 4]);
});
test("watch zero keys", function () {
var ledger = new index_1.default(mockOptions);
var stream = ledger.streamFetchByKeys(Foo, []);
stream.close();
var change = jest.fn();
stream.on("change", function (state) { return change(state); });
expect(change).toHaveBeenCalledTimes(1);
expect(change).toHaveBeenCalledWith([]);
mockInstance.serverSend({ events: [1, 2, 3].map(fooEvent) });
expect(change).toHaveBeenCalledWith([]);
expect(change).toHaveBeenCalledTimes(1);
});
});
//# sourceMappingURL=index.test.js.map