@ceramicnetwork/core
Version:
Typescript implementation of the Ceramic protocol
126 lines • 6.45 kB
JavaScript
import { EventType, StreamUtils, } from '@ceramicnetwork/common';
import cloneDeep from 'lodash.clonedeep';
import { SignatureUtils } from '@ceramicnetwork/stream-handler-common';
export class StateManipulator {
constructor(logger, streamTypeHandlers, logSyncer, context) {
this.logger = logger;
this.streamTypeHandlers = streamTypeHandlers;
this.logSyncer = logSyncer;
this.context = context;
}
async _applyLog(handler, state, log, throwOnInvalidCommit) {
for (const commit of log.commits) {
try {
state = await handler.applyCommit(commit, this.context, state);
}
catch (err) {
if (throwOnInvalidCommit || state == null) {
throw err;
}
else {
return state;
}
}
}
return state;
}
assertStreamTypeAppliable(streamType) {
this.streamTypeHandlers.get(streamType);
}
async applyFullLog(streamType, log, opts) {
if (log.commits.length < 1) {
throw new Error(`Log must contain at least one commit to apply`);
}
const handler = this.streamTypeHandlers.get(streamType);
return this._applyLog(handler, null, log, opts.throwOnInvalidCommit);
}
_copyTrustedTimestamps(source, dest, copyTimestampsFromRemovedAnchors) {
let timestamp = null;
for (let i = dest.commits.length - 1; i >= 0; i--) {
if (copyTimestampsFromRemovedAnchors || source[i].type == EventType.TIME) {
timestamp = source[i].timestamp;
}
if (!source[i].cid.equals(dest.commits[i].cid)) {
throw new Error(`Source and dest logs don't correspond!`);
}
dest.commits[i].expirationTime = source[i].expirationTime;
dest.commits[i].timestamp = timestamp;
}
return { commits: dest.commits, timestampStatus: 'validated' };
}
_findAnchorIndex(log) {
return log.findIndex((logEntry) => logEntry.type == EventType.TIME);
}
_pickLogToAccept(log1, log2) {
const firstAnchorIndexForLog1 = this._findAnchorIndex(log1);
const firstAnchorIndexForLog2 = this._findAnchorIndex(log2);
const isLog1Anchored = firstAnchorIndexForLog1 >= 0;
const isLog2Anchored = firstAnchorIndexForLog2 >= 0;
if (isLog1Anchored != isLog2Anchored) {
return isLog1Anchored ? log1 : log2;
}
if (isLog1Anchored && isLog2Anchored) {
const anchorTimestamp1 = log1[firstAnchorIndexForLog1].timestamp;
const anchorTimestamp2 = log2[firstAnchorIndexForLog2].timestamp;
if (anchorTimestamp1 < anchorTimestamp2) {
return log1;
}
else if (anchorTimestamp2 < anchorTimestamp1) {
return log2;
}
}
const relevantLength1 = isLog1Anchored ? firstAnchorIndexForLog1 + 1 : log1.length;
const relevantLength2 = isLog2Anchored ? firstAnchorIndexForLog2 + 1 : log2.length;
if (relevantLength1 > relevantLength2) {
return log1;
}
else if (relevantLength1 < relevantLength2) {
return log2;
}
return log1[log1.length - 1].cid.bytes < log2[log2.length - 1].cid.bytes ? log1 : log2;
}
async _applyLogToStateWithoutCacaoVerification(initialState, logToApply, opts) {
if (logToApply.commits.length == 0) {
return initialState;
}
const handler = this.streamTypeHandlers.get(initialState.type);
const firstNewCommit = logToApply.commits[0].commit;
const initialTip = initialState.log[initialState.log.length - 1].cid;
if (firstNewCommit.prev.equals(initialTip)) {
return this._applyLog(handler, cloneDeep(initialState), logToApply, opts.throwOnInvalidCommit);
}
const conflictingTip = logToApply.commits[logToApply.commits.length - 1].cid;
const streamId = StreamUtils.streamIdFromState(initialState);
if (opts.throwIfStale) {
throw new Error(`Commit to stream ${streamId.toString()} rejected because it builds on stale state. Calling 'sync()' on the stream handle will synchronize the stream state in the client with that on the Ceramic node. Rejected commit CID: ${conflictingTip}. Current tip: ${initialTip}`);
}
const conflictIdx = initialState.log.findIndex((entry) => entry.cid.equals(firstNewCommit.prev));
const localConflictingLog = initialState.log.slice(conflictIdx + 1);
const selectedLog = this._pickLogToAccept(localConflictingLog, logToApply.commits);
if (selectedLog == localConflictingLog) {
if (opts.throwOnConflict) {
throw new Error(`Commit to stream ${streamId.toString()} rejected by conflict resolution. Rejected commit CID: ${conflictingTip.toString()}. Current tip: ${initialTip.toString()}`);
}
return initialState;
}
const sharedState = await this.resetStateToCommit(initialState, initialState.log[conflictIdx].cid, { copyTimestampsFromRemovedAnchors: false });
return this._applyLog(handler, sharedState, logToApply, opts.throwOnInvalidCommit);
}
async resetStateToCommit(initialState, commitCid, opts) {
const streamId = StreamUtils.streamIdFromState(initialState);
const commitIndex = initialState.log.findIndex((logEntry) => logEntry.cid.equals(commitCid));
if (commitIndex < 0) {
throw new Error(`Requested commit CID ${commitCid.toString()} not found in the log for stream ${streamId.toString()}`);
}
const sharedLogWithoutTimestamps = await this.logSyncer.syncFullLog(streamId, commitCid);
const sharedLogWithTimestamps = this._copyTrustedTimestamps(initialState.log, sharedLogWithoutTimestamps, opts.copyTimestampsFromRemovedAnchors);
const handler = this.streamTypeHandlers.get(initialState.type);
return this._applyLog(handler, null, sharedLogWithTimestamps, true);
}
async applyLogToState(initialState, logToApply, opts) {
const state = await this._applyLogToStateWithoutCacaoVerification(initialState, logToApply, opts);
SignatureUtils.checkForCacaoExpiration(state);
return state;
}
}
//# sourceMappingURL=state-manipulator.js.map