in src/governance/ccf-app/js/src/endpoints/events.ts [35:225]
export function getEvent(
request: ccfapp.Request
): ccfapp.Response<GetEventsResponse> {
const contractId = request.params.contractId;
const parsedQuery = parseRequestQuery(request);
const id = getIdFromQuery(parsedQuery, contractId);
let { from_seqno, to_seqno, max_seqno_per_page, scope } = parsedQuery;
if (!scope) {
scope = "";
}
const eventsMap = ccfapp.typedKv(
getEventsMapName(contractId, scope),
ccfapp.string,
ccfapp.json<EventStoreItem>()
);
// If no sequence numbers are specified then return the latest value, if any.
if (!from_seqno && !to_seqno) {
if (!eventsMap.has(id)) {
return {
body: {
value: []
}
};
}
const item = eventsMap.get(id);
return {
body: {
value: [
{
scope: scope,
id: id,
seqno: eventsMap.getVersionOfPreviousWrite(id),
timestamp: item.timestamp,
timestamp_iso: new Date(Number(item.timestamp)).toISOString(),
data: item.data
}
]
}
};
}
if (from_seqno !== undefined) {
from_seqno = parseInt(from_seqno);
if (isNaN(from_seqno)) {
throw new Error("from_seqno is not an integer");
}
} else {
// If no from_seqno is specified, defaults to very first transaction in the ledger.
from_seqno = 1;
}
if (to_seqno !== undefined) {
to_seqno = parseInt(to_seqno);
if (isNaN(to_seqno)) {
throw new Error("to_seqno is not an integer");
}
} else {
// If no end point is specified, use the last time this ID was written to.
const lastWriteVersion = eventsMap.getVersionOfPreviousWrite(id);
if (lastWriteVersion !== undefined) {
to_seqno = lastWriteVersion;
} else {
// If there's no last written version, it may have never been
// written but may simply be currently deleted. Use current commit
// index as end point to ensure we include any deleted entries.
to_seqno = ccf.consensus.getLastCommittedTxId().seqno;
}
}
// Range must be in order.
if (to_seqno < from_seqno) {
throw new Error("to_seqno must be >= from_seqno");
}
if (max_seqno_per_page !== undefined) {
max_seqno_per_page = parseInt(max_seqno_per_page);
if (isNaN(max_seqno_per_page)) {
throw new Error("max_seqno_per_page is not an integer");
}
} else {
// If no max_seqno_per_page is specified, defaults to 2000.
max_seqno_per_page = 2000;
}
// End of range must be committed.
let isCommitted = false;
const viewOfFinalSeqno = ccf.consensus.getViewForSeqno(to_seqno);
if (viewOfFinalSeqno !== null) {
const txStatus = ccf.consensus.getStatusForTxId(viewOfFinalSeqno, to_seqno);
isCommitted = txStatus === "Committed";
}
if (!isCommitted) {
throw new Error("End of range must be committed");
}
const rangeBegin = from_seqno;
const rangeEnd = Math.min(to_seqno, rangeBegin + max_seqno_per_page);
// Compute a deterministic handle for the range request.
// Note: Instead of ccf.digest, an equivalent of std::hash should be used.
const makeHandle = (begin, end, id) => {
const cacheKey = `${begin}-${end}-${id}`;
const digest = ccf.crypto.digest("SHA-256", ccf.strToBuf(cacheKey));
const handle = new DataView(digest).getUint32(0);
return handle;
};
const handle = makeHandle(rangeBegin, rangeEnd, id);
// Fetch the requested range.
const expirySeconds = 1800;
const states = ccf.historical.getStateRange(
handle,
rangeBegin,
rangeEnd,
expirySeconds
);
if (states === null) {
return {
statusCode: 202,
headers: {
"retry-after": "1"
}
};
}
// Process the fetched states.
const entries: Event[] = [];
for (const state of states) {
const eventsMapHistorical = ccfapp.typedKv(
state.kv[getEventsMapName(contractId, scope)],
ccfapp.string,
ccfapp.json<EventStoreItem>()
);
const item = eventsMapHistorical.get(id);
if (item !== undefined) {
entries.push({
scope: scope,
id: id,
seqno: parseInt(state.transactionId.split(".")[1]),
timestamp: item.timestamp,
timestamp_iso: new Date(Number(item.timestamp)).toISOString(),
data: item.data
});
}
// This response does not include any entry when the given key wasn't
// modified at this seqno. It could instead indicate that the store
// was checked with an empty tombstone object, but this approach gives
// smaller responses.
}
// If this didn't cover the total requested range, begin fetching the
// next page and tell the caller how to retrieve it.
let nextLink;
if (rangeEnd != to_seqno) {
const next_page_start = rangeEnd + 1;
const next_page_end = Math.min(
to_seqno,
next_page_start + max_seqno_per_page
);
const next_page_handle = makeHandle(next_page_start, next_page_end, id);
ccf.historical.getStateRange(
next_page_handle,
next_page_start,
next_page_end,
expirySeconds
);
// NB: This path tells the caller to continue to ask until the end of
// the range, even if the next response is paginated.
const nextLinkPrefix = `/app/contracts/${contractId}/events`;
nextLink = `${nextLinkPrefix}?from_seqno=${next_page_start}&to_seqno=${to_seqno}&id=${id}`;
if (scope !== undefined) {
nextLink += `&scope=${scope}`;
}
}
// Assume this response makes it all the way to the client, and
// they're finished with it, so we can drop the retrieved state. Note: Consider if
// this may be driven by a separate client request or an LRU.
ccf.historical.dropCachedStates(handle);
return {
body: {
value: entries,
nextLink: nextLink
}
};
}