@ceramicnetwork/core
Version:
Typescript implementation of the Ceramic protocol
551 lines • 27.6 kB
JavaScript
import { Dispatcher } from './dispatcher.js';
import { StreamID, CommitID, StreamRef } from '@ceramicnetwork/streamid';
import { IpfsTopology } from '@ceramicnetwork/ipfs-topology';
import { EnvironmentUtils, StreamUtils, LoggerProvider, SyncOptions, CeramicSigner, delayOrAbort, } from '@ceramicnetwork/common';
import { DEFAULT_TRACE_SAMPLE_RATIO, ServiceMetrics as Metrics, } from '@ceramicnetwork/observability';
import { DEFAULT_PUBLISH_INTERVAL_MS, NodeMetrics } from '@ceramicnetwork/node-metrics';
import { PinStoreFactory } from './store/pin-store-factory.js';
import { PathTrie, promiseTimeout } from './utils.js';
import { LocalPinApi } from './local-pin-api.js';
import { LocalAdminApi } from './local-admin-api.js';
import { Repository } from './state-management/repository.js';
import { HandlersMap } from './handlers-map.js';
import { streamFromState } from './state-management/stream-from-state.js';
import * as fs from 'fs';
import os from 'os';
import * as path from 'path';
import { LocalIndexApi, SyncApi } from '@ceramicnetwork/indexing';
import { ShutdownSignal } from './shutdown-signal.js';
import { AnchorRequestStore } from './store/anchor-request-store.js';
import { ProvidersCache } from './providers-cache.js';
import crypto from 'crypto';
import { networkOptionsByName, } from './initialization/network-options.js';
import { usableAnchorChains, makeAnchorService, makeEthereumRpcUrl, } from './initialization/anchoring.js';
import { makeStreamLoaderAndUpdater } from './initialization/stream-loading.js';
import { ReconApi } from './recon.js';
import { LevelKVFactory } from './store/level-kv-factory.js';
const METRICS_CALLER_NAME = 'js-ceramic';
const DEFAULT_CACHE_LIMIT = 500;
const DEFAULT_QPS_LIMIT = 10;
const DEFAULT_MULTIQUERY_TIMEOUT_MS = 30 * 1000;
const DEFAULT_MIN_ANCHOR_LOOP_DURATION_MS = 60 * 1000;
const TESTING = process.env.NODE_ENV == 'test';
const DEFAULT_CLIENT_INITIATED_WRITE_OPTS = {
throwOnInvalidCommit: true,
throwOnConflict: true,
throwIfStale: true,
};
const DEFAULT_APPLY_COMMIT_OPTS = {
anchor: true,
publish: true,
sync: SyncOptions.PREFER_CACHE,
...DEFAULT_CLIENT_INITIATED_WRITE_OPTS,
};
const DEFAULT_CREATE_FROM_GENESIS_OPTS = {
anchor: true,
publish: true,
sync: SyncOptions.PREFER_CACHE,
...DEFAULT_CLIENT_INITIATED_WRITE_OPTS,
};
const DEFAULT_LOAD_OPTS = { sync: SyncOptions.PREFER_CACHE };
export const DEFAULT_STATE_STORE_DIRECTORY = path.join(os.homedir(), '.ceramic', 'statestore');
const ERROR_LOADING_STREAM = 'error_loading_stream';
const ERROR_APPLYING_COMMIT = 'error_applying_commit';
const VERSION_INFO = 'version_info';
const PUBLISH_VERSION_INTERVAL_MS = 1000 * 60 * 60;
const normalizeStreamID = (streamId) => {
const streamRef = StreamRef.from(streamId);
if (StreamID.isInstance(streamRef)) {
return streamRef;
}
else {
throw new Error(`Not StreamID: ${streamRef}`);
}
};
const tryStreamId = (id) => {
try {
return StreamID.fromString(id);
}
catch (e) {
return null;
}
};
export class Ceramic {
constructor(modules, params) {
this._versionMetricInterval = undefined;
this._signer = modules.signer;
this._ipfsTopology = modules.ipfsTopology;
this.loggerProvider = modules.loggerProvider;
this._logger = modules.loggerProvider.getDiagnosticsLogger();
this.repository = modules.repository;
this.feed = modules.feed;
this._shutdownSignal = modules.shutdownSignal;
this.dispatcher = modules.dispatcher;
this.anchorService = modules.anchorService;
this.providersCache = modules.providersCache;
this._readOnly = params.readOnly;
this._metricsConfig = params.metrics;
this._networkOptions = params.networkOptions;
this._loadOptsOverride = params.loadOptsOverride;
this._versionInfo = params.versionInfo;
this._runId = crypto.randomUUID();
this._startTime = new Date();
this._kvFactory = new LevelKVFactory(params.stateStoreDirectory ?? DEFAULT_STATE_STORE_DIRECTORY, this._networkOptions.name, this._logger);
this._ipfs = modules.ipfs;
this._streamHandlers = new HandlersMap(this._logger);
const [streamLoader, streamUpdater] = makeStreamLoaderAndUpdater(this._logger, this.dispatcher, modules.anchorService.validator, this, this._streamHandlers);
const pinStore = modules.pinStoreFactory.createPinStore();
const localIndex = new LocalIndexApi(params.indexingConfig, this, this, this._logger, params.networkOptions.name);
const anchorRequestStore = new AnchorRequestStore(this._logger, params.anchorLoopMinDurationMs >= 0
? params.anchorLoopMinDurationMs
: DEFAULT_MIN_ANCHOR_LOOP_DURATION_MS);
this.repository.setDeps({
dispatcher: this.dispatcher,
pinStore: pinStore,
kvFactory: this._kvFactory,
anchorRequestStore,
handlers: this._streamHandlers,
anchorService: modules.anchorService,
indexing: localIndex,
streamLoader,
streamUpdater,
});
this.syncApi = new SyncApi({
db: params.indexingConfig.db,
on: params.sync,
}, this.dispatcher, this.repository.handleUpdateFromNetwork.bind(this.repository), this.repository.index, this._logger);
const pinApi = new LocalPinApi(this.repository, this._logger);
this.repository.index.setSyncQueryApi(this.syncApi);
this.recon = modules.reconApi;
this.admin = new LocalAdminApi(this._logger, localIndex, this.syncApi, this.nodeStatus.bind(this), pinApi, this.providersCache, this.loadStream.bind(this), modules.reconApi);
}
get index() {
return this.repository.index;
}
get pubsubTopic() {
return this._networkOptions.pubsubTopic;
}
get signer() {
return this._signer;
}
get ipfs() {
return this._ipfs;
}
get did() {
return this._did;
}
set did(did) {
this._did = did;
this._signer.withDid(did);
}
static _processConfig(ipfs, config, versionInfo) {
const loggerProvider = config.loggerProvider ?? new LoggerProvider();
const logger = loggerProvider.getDiagnosticsLogger();
const pubsubLogger = loggerProvider.makeServiceLogger('pubsub');
const networkOptions = networkOptionsByName(config.networkName, config.pubsubTopic, config.networkId);
const ethereumRpcUrl = makeEthereumRpcUrl(config.ethereumRpcUrl, networkOptions.name, logger);
const signer = CeramicSigner.invalid();
const anchorService = makeAnchorService(config, ethereumRpcUrl, networkOptions.name, logger, versionInfo, signer);
const providersCache = new ProvidersCache(ethereumRpcUrl);
const loadOptsOverride = config.syncOverride ? { sync: config.syncOverride } : {};
const streamCacheLimit = process.env.CERAMIC_STREAM_CACHE_LIMIT
? parseInt(process.env.CERAMIC_STREAM_CACHE_LIMIT)
: config.streamCacheLimit ?? DEFAULT_CACHE_LIMIT;
const concurrentRequestsLimit = config.concurrentRequestsLimit ?? streamCacheLimit;
const maxQueriesPerSecond = process.env.CERAMIC_PUBSUB_QPS_LIMIT
? parseInt(process.env.CERAMIC_PUBSUB_QPS_LIMIT)
: DEFAULT_QPS_LIMIT;
const ipfsTopology = new IpfsTopology(ipfs, networkOptions.name, logger);
const reconApi = new ReconApi({
enabled: EnvironmentUtils.useRustCeramic(),
url: ipfs.config.get('Addresses.API').then((url) => url.toString()),
feedEnabled: config.reconFeedEnabled ?? true,
network: networkOptions.name,
}, logger);
const repository = new Repository(streamCacheLimit, concurrentRequestsLimit, reconApi, logger);
const shutdownSignal = new ShutdownSignal();
const dispatcher = new Dispatcher(ipfs, networkOptions.pubsubTopic, repository, logger, pubsubLogger, shutdownSignal, !config.disablePeerDataSync, maxQueriesPerSecond, reconApi);
const pinStoreOptions = {
pinningEndpoints: config.ipfsPinningEndpoints,
pinningBackends: config.pinningBackends,
};
const pinStoreFactory = new PinStoreFactory(ipfs, dispatcher.ipldCache, repository, pinStoreOptions, logger);
const params = {
readOnly: config.readOnly,
stateStoreDirectory: config.stateStoreDirectory,
indexingConfig: config.indexing,
metrics: config.metrics,
networkOptions,
loadOptsOverride,
sync: config.indexing?.enableHistoricalSync,
anchorLoopMinDurationMs: parseInt(config.anchorLoopMinDurationMs, 10),
versionInfo,
};
const modules = {
anchorService,
dispatcher,
ipfs,
ipfsTopology,
loggerProvider,
pinStoreFactory,
repository,
shutdownSignal,
providersCache,
feed: repository.feed,
signer,
reconApi,
};
return [modules, params];
}
static async create(ipfs, config = {}, versionInfo) {
const [modules, params] = Ceramic._processConfig(ipfs, config, versionInfo);
const ceramic = new Ceramic(modules, params);
const doPeerDiscovery = config.useCentralizedPeerDiscovery ?? !TESTING;
await ceramic._init(doPeerDiscovery);
return ceramic;
}
async _init(doPeerDiscovery) {
try {
if (EnvironmentUtils.useRustCeramic()) {
this._logger.imp(`Connecting to ceramic network '${this._networkOptions.name}' using ceramic-one with Recon for data synchronization.`);
}
else {
this._logger.imp(`Connecting to ceramic network '${this._networkOptions.name}' using pubsub topic '${this._networkOptions.pubsubTopic}'`);
}
if (this._readOnly) {
this._logger.warn(`Starting in read-only mode. All write operations will fail`);
}
await this.repository.init();
await this.dispatcher.init();
if (doPeerDiscovery) {
await this._ipfsTopology.start();
}
if (!this._readOnly) {
await this.anchorService.init(this.repository.anchorRequestStore, this.repository.anchorLoopHandler());
this._supportedChains = await usableAnchorChains(this._networkOptions.name, this.anchorService, this._logger);
if (this.index.enabled && this.syncApi.enabled) {
const chainId = this._supportedChains[0];
if (chainId) {
const provider = await this.providersCache.getProvider(chainId);
await this.syncApi.init(provider);
}
}
}
await this._startupChecks();
await this._startMetrics();
}
catch (err) {
this._logger.err(err);
await this.close();
throw err;
}
}
async _publishVersionMetrics() {
Metrics.observe(VERSION_INFO, 1, {
jsCeramicVersion: this._versionInfo.cliPackageVersion,
jsCeramicGitHash: this._versionInfo.gitHash,
ceramicOneVersion: this._versionInfo.ceramicOneVersion,
});
}
async _startMetrics() {
const metricsExporterEnabled = this._metricsConfig?.metricsExporterEnabled && this._metricsConfig?.collectorHost;
const prometheusExporterEnabled = this._metricsConfig?.prometheusExporterEnabled && this._metricsConfig?.prometheusExporterPort;
if (metricsExporterEnabled && prometheusExporterEnabled) {
Metrics.start(this._metricsConfig.collectorHost, METRICS_CALLER_NAME, DEFAULT_TRACE_SAMPLE_RATIO, null, true, this._metricsConfig.prometheusExporterPort);
}
else if (metricsExporterEnabled) {
Metrics.start(this._metricsConfig.collectorHost, METRICS_CALLER_NAME);
}
else if (prometheusExporterEnabled) {
Metrics.start('', METRICS_CALLER_NAME, DEFAULT_TRACE_SAMPLE_RATIO, null, true, this._metricsConfig.prometheusExporterPort);
}
if (this.did?.authenticated) {
if (!this._metricsConfig || this._metricsConfig?.metricsPublisherEnabled) {
if (EnvironmentUtils.useRustCeramic()) {
const metricsModel = NodeMetrics.getModel(this._networkOptions.name);
void this._waitForMetricsModel(metricsModel).then(this._startPublishingNodeMetrics.bind(this, metricsModel));
}
else {
this._logger.warn(`Disabling publishing of Node Metrics because we are not connected to a Recon-compatible p2p node`);
}
}
else {
this._logger.imp(`Node Metrics publishing disabled by config`);
}
}
else {
this._logger.warn(`The ceramic daemon is running without an authenticated DID. This means that this node cannot itself publish streams, including node metrics, and cannot use a DID as the method to authenticate with the Ceramic Anchor Service. See https://developers.ceramic.network/docs/composedb/guides/composedb-server/access-mainnet#updating-to-did-based-authentication for instructions on how to update your node to use DID authentication.`);
}
this._versionMetricInterval = setInterval(this._publishVersionMetrics.bind(this), PUBLISH_VERSION_INTERVAL_MS);
}
async _startPublishingNodeMetrics(metricsModel) {
await this.repository.index.indexModels([{ streamID: metricsModel }]);
await this.recon.registerInterest(metricsModel, this.did.id);
const ipfsVersion = await this.ipfs.version();
const ipfsId = await this.ipfs.id();
NodeMetrics.start({
ceramic: this,
network: this._networkOptions.name,
ceramicVersion: this._versionInfo.cliPackageVersion,
ipfsVersion: ipfsVersion.version,
intervalMS: this._metricsConfig?.metricsPublishIntervalMS || DEFAULT_PUBLISH_INTERVAL_MS,
nodeId: ipfsId.id.toString(),
nodeName: '',
nodeAuthDID: this.did.id,
nodeIPAddr: '',
nodePeerId: ipfsId.id.toString(),
logger: this._logger,
});
this._logger.imp(`Publishing Node Metrics publicly to the Ceramic Network. To learn more, including how to disable publishing, please see the NODE_METRICS.md file for your branch, e.g. https://github.com/ceramicnetwork/js-ceramic/blob/develop/docs-dev/NODE_METRICS.md`);
}
async _waitForMetricsModel(model) {
let attemptNum = 0;
let backoffMs = 100;
const maxBackoffMs = 1000 * 60;
while (!this._shutdownSignal.isShuttingDown()) {
try {
await this.dispatcher.getFromIpfs(model.cid);
if (attemptNum > 0) {
this._logger.imp(`Model ${model} used to publish Node Metrics loaded successfully`);
}
return;
}
catch (err) {
if (attemptNum == 0) {
this._logger.imp(`Waiting for Model ${model} used to publish Node Metrics to be available locally`);
}
else if (attemptNum % 5 == 0) {
this._logger.err(`Error loading Model ${model} used to publish Node Metrics: ${err}`);
}
await this._shutdownSignal.abortable((signal) => delayOrAbort(backoffMs, signal));
attemptNum++;
if (backoffMs <= maxBackoffMs) {
backoffMs *= 2;
}
}
}
}
async _startupChecks() {
await this._checkIPFSPersistence();
if (!this.dispatcher.enableSync && this.index.enabled && this.syncApi.enabled) {
throw new Error(`Cannot enable ComposeDB Historical Data Sync while IPFS peer data sync is disabled`);
}
if (!EnvironmentUtils.useRustCeramic() && !this.dispatcher.enableSync) {
this._logger.warn(`IPFS peer data sync is disabled. This node will be unable to load data from any other Ceramic nodes on the network`);
}
}
async _checkIPFSPersistence() {
if (process.env.CERAMIC_SKIP_IPFS_PERSISTENCE_STARTUP_CHECK == 'true') {
this._logger.warn(`Skipping IPFS persistence checks`);
return;
}
const state = await this.repository.randomPinnedStreamState();
if (!state) {
this._logger.warn(`No pinned streams detected. This is expected if this is the first time this node has been run, but may indicate a problem with the node's persistence setup if it should have pinned streams`);
return;
}
const commitCIDs = [state.log[0].cid];
if (state.log.length > 1) {
commitCIDs.push(state.log[state.log.length - 1].cid);
}
for (const cid of commitCIDs) {
const cidFound = await this.dispatcher.cidExistsInLocalIPFSStore(cid);
if (!cidFound) {
const streamID = StreamUtils.streamIdFromState(state).toString();
if (!process.env.IPFS_PATH && fs.existsSync(path.resolve(os.homedir(), '.jsipfs'))) {
throw new Error(`IPFS data missing! The CID ${cid} of pinned Stream ${streamID} is missing from the local IPFS node. This means that pinned content has gone missing from the IPFS node. A ~/.jsipfs directory has been detected which may mean you have not completed the steps needed to upgrade to js-ceramic v2. Please follow the upgrade guide found here: https://threebox.notion.site/Upgrading-to-js-ceramic-v2-Filesystem-b6b3cbb989a34e05893761fd914965b7. If that does not work check your IPFS node configuration and make sure it is pointing to the proper repo`);
}
throw new Error(`IPFS data missing! The CID ${cid} of pinned Stream ${streamID} is missing from the local IPFS node. This means that pinned content has gone missing from the IPFS node. Check your IPFS node configuration and make sure it is pointing to the proper repo`);
}
}
}
addStreamHandler(streamHandler) {
this._streamHandlers.add(streamHandler);
}
async nodeStatus() {
const anchor = {
anchorServiceUrl: this.anchorService.url,
ethereumRpcEndpoint: this.anchorService.validator.ethereumRpcEndpoint,
chainId: this.anchorService.validator.chainId,
};
const ipfsStatus = await this.dispatcher.ipfsNodeStatus();
let composeDB = undefined;
if (this.repository.index.enabled) {
composeDB = {
indexedModels: this.repository.index.indexedModels().map((stream) => stream.toString()),
};
if (this.syncApi.enabled) {
composeDB.syncs = await this.syncApi.syncStatus();
}
}
return {
runId: this._runId,
uptimeMs: new Date().getTime() - this._startTime.getTime(),
network: this._networkOptions.name,
anchor,
ipfs: ipfsStatus,
composeDB,
};
}
async applyCommit(streamId, commit, opts = {}) {
if (this._readOnly) {
throw new Error('Writes to streams are not supported in readOnly mode');
}
opts = { ...DEFAULT_APPLY_COMMIT_OPTS, ...opts, ...this._loadOptsOverride };
const id = normalizeStreamID(streamId);
this._logger.verbose(`Apply commit to stream ${id.toString()}`);
try {
const state$ = await this.repository.applyCommit(id, commit, opts);
this._logger.verbose(`Applied commit to stream ${id.toString()}`);
return streamFromState(this, this._streamHandlers, state$.value, this.repository.updates$);
}
catch (err) {
this._logger.err(`Error applying commit to stream ${streamId.toString()}: ${err}`);
Metrics.count(ERROR_APPLYING_COMMIT, 1);
NodeMetrics.recordError(ERROR_APPLYING_COMMIT);
throw err;
}
}
async requestAnchor(streamId, opts = {}) {
opts = { ...DEFAULT_LOAD_OPTS, ...opts, ...this._loadOptsOverride };
const effectiveStreamId = normalizeStreamID(streamId);
const state = await this.repository.load(effectiveStreamId, opts);
await this.repository.anchor(state, opts);
return state.state.anchorStatus;
}
async createStreamFromGenesis(type, genesis, opts = {}) {
opts = { ...DEFAULT_CREATE_FROM_GENESIS_OPTS, ...opts, ...this._loadOptsOverride };
const state$ = await this.repository.createStreamFromGenesis(type, genesis, opts);
const stream = streamFromState(this, this._streamHandlers, state$.value, this.repository.updates$);
this._logger.verbose(`Created stream ${stream.id.toString()} from state`);
return stream;
}
async loadStream(streamId, opts = {}) {
opts = { ...DEFAULT_LOAD_OPTS, ...opts, ...this._loadOptsOverride };
const streamRef = StreamRef.from(streamId);
if (CommitID.isInstance(streamRef)) {
const snapshot$ = await this.repository.loadAtCommit(streamRef, opts);
return streamFromState(this, this._streamHandlers, snapshot$.value);
}
else if (opts.atTime) {
const snapshot$ = await this.repository.loadAtTime(streamRef, opts);
return streamFromState(this, this._streamHandlers, snapshot$.value);
}
else {
try {
const base$ = await this.repository.load(streamRef.baseID, opts);
return streamFromState(this, this._streamHandlers, base$.value, this.repository.updates$);
}
catch (err) {
this._logger.err(`Error loading stream ${streamId.toString()}: ${err}`);
Metrics.count(ERROR_LOADING_STREAM, 1);
NodeMetrics.recordError(ERROR_LOADING_STREAM);
if (opts.sync != SyncOptions.SYNC_ON_ERROR) {
throw err;
}
this._logger.warn(`Error while loading stream ${streamRef.toString()} with SYNC_ON_ERROR flag. Resyncing stream. Error: ${err}`);
opts.sync = SyncOptions.SYNC_ALWAYS;
const base$ = await this.repository.load(streamRef.baseID, opts);
return streamFromState(this, this._streamHandlers, base$.value, this.repository.updates$);
}
}
}
async loadStreamState(streamId) {
return await this.repository.streamState(streamId);
}
async _ensureGenesis(genesis, streamRef, opts) {
if (StreamUtils.isSignedCommitContainer(genesis) || StreamUtils.isSignedCommit(genesis)) {
throw new Error('Given genesis commit is not deterministic');
}
const stream = await this.createStreamFromGenesis(streamRef.type, genesis, opts);
if (!streamRef.equals(stream.id)) {
throw new Error(`StreamID ${stream.id.toString()} generated does not match expected StreamID ${streamRef.toString()} determined from genesis content in multiquery request`);
}
}
async _loadLinkedStreams(query, timeout) {
const id = StreamRef.from(query.streamId);
const pathTrie = new PathTrie();
query.paths?.forEach((path) => pathTrie.add(path));
if (query.genesis) {
await this._ensureGenesis(query.genesis, id, query.opts);
}
const index = {};
const walkNext = async (node, streamId) => {
const opts = query.opts ?? {};
let stream;
try {
stream = await promiseTimeout(this.loadStream(streamId, opts), timeout, `Timeout after ${timeout}ms`);
}
catch (e) {
if (CommitID.isInstance(streamId)) {
this._logger.warn(`Error loading stream ${streamId.baseID.toString()} at commit ${streamId.commit.toString()} at time ${opts.atTime} as part of a multiQuery request: ${e.toString()}`);
}
else {
this._logger.warn(`Error loading stream ${streamId.toString()} at time ${opts.atTime} as part of a multiQuery request: ${e.toString()}`);
}
Metrics.count(ERROR_LOADING_STREAM, 1);
NodeMetrics.recordError(ERROR_LOADING_STREAM);
return Promise.resolve();
}
const streamRef = opts.atTime ? CommitID.make(streamId.baseID, stream.tip) : streamId;
index[streamRef.toString()] = stream;
const promiseList = Object.keys(node.children).map((key) => {
const keyStreamId = stream.content[key] ? tryStreamId(stream.content[key]) : null;
if (keyStreamId)
return walkNext(node.children[key], keyStreamId);
return Promise.resolve();
});
await Promise.all(promiseList);
};
await walkNext(pathTrie.root, id);
return index;
}
async multiQuery(queries, timeout = DEFAULT_MULTIQUERY_TIMEOUT_MS) {
const queryResults = await Promise.all(queries.map((query) => {
return this._loadLinkedStreams(query, timeout).catch((e) => {
this._logger.warn(`Error during multiQuery: ${e.toString()}`);
return {};
});
}));
const results = queryResults.reduce((acc, res) => ({ ...acc, ...res }), {});
await Promise.all(Object.values(results).map((stream) => {
if (!stream.isReadOnly) {
return stream.sync({ sync: SyncOptions.NEVER_SYNC, syncTimeoutSeconds: 0 });
}
}));
return results;
}
async loadStreamCommits(streamId) {
const effectiveStreamId = normalizeStreamID(streamId);
const stream = await this.loadStream(effectiveStreamId);
const { state } = stream;
const results = await Promise.all(state.log.map(async ({ cid }) => {
const commit = await this.dispatcher.retrieveCommit(cid, effectiveStreamId);
return {
cid: cid.toString(),
value: await StreamUtils.convertCommitToSignedCommitContainer(commit, this.ipfs),
};
}));
this._logger.verbose(`Successfully loaded ${results.length} commits for stream ${streamId.toString()}`);
return results;
}
async getSupportedChains() {
return this._supportedChains;
}
buildStreamFromState(state) {
return streamFromState(this, this._streamHandlers, state, this.repository.updates$);
}
async close() {
this._logger.imp('Closing Ceramic instance');
clearInterval(this._versionMetricInterval);
await this.anchorService.close();
this._shutdownSignal.abort();
await this.syncApi.shutdown();
await this.dispatcher.close();
await this.repository.close();
this._ipfsTopology.stop();
await this._kvFactory.close();
this._logger.imp('Ceramic instance closed successfully');
}
}
//# sourceMappingURL=ceramic.js.map