UNPKG

@ceramicnetwork/core

Version:

Typescript implementation of the Ceramic protocol

126 lines 6.45 kB
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