@helia/bitswap 0.0.0-329652a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -0
- package/README.md +64 -0
- package/dist/index.min.js +3 -0
- package/dist/src/bitswap.d.ts +50 -0
- package/dist/src/bitswap.d.ts.map +1 -0
- package/dist/src/bitswap.js +120 -0
- package/dist/src/bitswap.js.map +1 -0
- package/dist/src/constants.d.ts +12 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +12 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/index.d.ts +178 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +12 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/network.d.ts +84 -0
- package/dist/src/network.d.ts.map +1 -0
- package/dist/src/network.js +370 -0
- package/dist/src/network.js.map +1 -0
- package/dist/src/pb/message.d.ts +67 -0
- package/dist/src/pb/message.d.ts.map +1 -0
- package/dist/src/pb/message.js +359 -0
- package/dist/src/pb/message.js.map +1 -0
- package/dist/src/peer-want-lists/index.d.ts +44 -0
- package/dist/src/peer-want-lists/index.d.ts.map +1 -0
- package/dist/src/peer-want-lists/index.js +116 -0
- package/dist/src/peer-want-lists/index.js.map +1 -0
- package/dist/src/peer-want-lists/ledger.d.ts +54 -0
- package/dist/src/peer-want-lists/ledger.d.ts.map +1 -0
- package/dist/src/peer-want-lists/ledger.js +104 -0
- package/dist/src/peer-want-lists/ledger.js.map +1 -0
- package/dist/src/session.d.ts +20 -0
- package/dist/src/session.d.ts.map +1 -0
- package/dist/src/session.js +100 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/stats.d.ts +16 -0
- package/dist/src/stats.d.ts.map +1 -0
- package/dist/src/stats.js +49 -0
- package/dist/src/stats.js.map +1 -0
- package/dist/src/utils/cid-prefix.d.ts +3 -0
- package/dist/src/utils/cid-prefix.d.ts.map +1 -0
- package/dist/src/utils/cid-prefix.js +7 -0
- package/dist/src/utils/cid-prefix.js.map +1 -0
- package/dist/src/utils/varint-decoder.d.ts +3 -0
- package/dist/src/utils/varint-decoder.d.ts.map +1 -0
- package/dist/src/utils/varint-decoder.js +15 -0
- package/dist/src/utils/varint-decoder.js.map +1 -0
- package/dist/src/utils/varint-encoder.d.ts +3 -0
- package/dist/src/utils/varint-encoder.d.ts.map +1 -0
- package/dist/src/utils/varint-encoder.js +14 -0
- package/dist/src/utils/varint-encoder.js.map +1 -0
- package/dist/src/want-list.d.ts +120 -0
- package/dist/src/want-list.d.ts.map +1 -0
- package/dist/src/want-list.js +361 -0
- package/dist/src/want-list.js.map +1 -0
- package/package.json +200 -0
- package/src/bitswap.ts +152 -0
- package/src/constants.ts +11 -0
- package/src/index.ts +215 -0
- package/src/network.ts +506 -0
- package/src/pb/message.proto +42 -0
- package/src/pb/message.ts +450 -0
- package/src/peer-want-lists/index.ts +165 -0
- package/src/peer-want-lists/ledger.ts +161 -0
- package/src/session.ts +150 -0
- package/src/stats.ts +67 -0
- package/src/utils/cid-prefix.ts +8 -0
- package/src/utils/varint-decoder.ts +19 -0
- package/src/utils/varint-encoder.ts +18 -0
- package/src/want-list.ts +529 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* eslint-disable max-depth */
|
|
2
|
+
import { DEFAULT_MAX_SIZE_REPLACE_HAS_WITH_BLOCK } from '../constants.js'
|
|
3
|
+
import { BlockPresenceType, type BitswapMessage, WantType } from '../pb/message.js'
|
|
4
|
+
import { cidToPrefix } from '../utils/cid-prefix.js'
|
|
5
|
+
import type { Network } from '../network.js'
|
|
6
|
+
import type { PeerId } from '@libp2p/interface'
|
|
7
|
+
import type { Blockstore } from 'interface-blockstore'
|
|
8
|
+
import type { AbortOptions } from 'it-length-prefixed-stream'
|
|
9
|
+
import type { CID } from 'multiformats/cid'
|
|
10
|
+
|
|
11
|
+
export interface LedgerComponents {
|
|
12
|
+
peerId: PeerId
|
|
13
|
+
blockstore: Blockstore
|
|
14
|
+
network: Network
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LedgerInit {
|
|
18
|
+
maxSizeReplaceHasWithBlock?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PeerWantListEntry {
|
|
22
|
+
/**
|
|
23
|
+
* The CID the peer has requested
|
|
24
|
+
*/
|
|
25
|
+
cid: CID
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The priority with which the remote should return the block
|
|
29
|
+
*/
|
|
30
|
+
priority: number
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* If we want the block or if we want the remote to tell us if they have the
|
|
34
|
+
* block - note if the block is small they'll send it to us anyway.
|
|
35
|
+
*/
|
|
36
|
+
wantType: WantType
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether the remote should tell us if they have the block or not
|
|
40
|
+
*/
|
|
41
|
+
sendDontHave: boolean
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* If we don't have the block and we've told them we don't have the block
|
|
45
|
+
*/
|
|
46
|
+
sentDontHave?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class Ledger {
|
|
50
|
+
public peerId: PeerId
|
|
51
|
+
private readonly blockstore: Blockstore
|
|
52
|
+
private readonly network: Network
|
|
53
|
+
public wants: Map<string, PeerWantListEntry>
|
|
54
|
+
public exchangeCount: number
|
|
55
|
+
public bytesSent: number
|
|
56
|
+
public bytesReceived: number
|
|
57
|
+
public lastExchange?: number
|
|
58
|
+
private readonly maxSizeReplaceHasWithBlock: number
|
|
59
|
+
|
|
60
|
+
constructor (components: LedgerComponents, init: LedgerInit) {
|
|
61
|
+
this.peerId = components.peerId
|
|
62
|
+
this.blockstore = components.blockstore
|
|
63
|
+
this.network = components.network
|
|
64
|
+
this.wants = new Map()
|
|
65
|
+
|
|
66
|
+
this.exchangeCount = 0
|
|
67
|
+
this.bytesSent = 0
|
|
68
|
+
this.bytesReceived = 0
|
|
69
|
+
this.maxSizeReplaceHasWithBlock = init.maxSizeReplaceHasWithBlock ?? DEFAULT_MAX_SIZE_REPLACE_HAS_WITH_BLOCK
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
sentBytes (n: number): void {
|
|
73
|
+
this.exchangeCount++
|
|
74
|
+
this.lastExchange = (new Date()).getTime()
|
|
75
|
+
this.bytesSent += n
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
receivedBytes (n: number): void {
|
|
79
|
+
this.exchangeCount++
|
|
80
|
+
this.lastExchange = (new Date()).getTime()
|
|
81
|
+
this.bytesReceived += n
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
debtRatio (): number {
|
|
85
|
+
return (this.bytesSent / (this.bytesReceived + 1)) // +1 is to prevent division by zero
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async sendBlocksToPeer (options?: AbortOptions): Promise<void> {
|
|
89
|
+
const message: Pick<BitswapMessage, 'blockPresences' | 'blocks'> = {
|
|
90
|
+
blockPresences: [],
|
|
91
|
+
blocks: []
|
|
92
|
+
}
|
|
93
|
+
const sentBlocks = new Set<string>()
|
|
94
|
+
|
|
95
|
+
for (const [key, entry] of this.wants.entries()) {
|
|
96
|
+
const has = await this.blockstore.has(entry.cid, options)
|
|
97
|
+
|
|
98
|
+
if (!has) {
|
|
99
|
+
// we don't have the requested block and the remote is not interested
|
|
100
|
+
// in us telling them that
|
|
101
|
+
if (!entry.sendDontHave) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// we have already told them we don't have the block
|
|
106
|
+
if (entry.sentDontHave === true) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
entry.sentDontHave = true
|
|
111
|
+
message.blockPresences.push({
|
|
112
|
+
cid: entry.cid.bytes,
|
|
113
|
+
type: BlockPresenceType.DontHaveBlock
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const block = await this.blockstore.get(entry.cid, options)
|
|
120
|
+
|
|
121
|
+
// do they want the block or just us to tell them we have the block
|
|
122
|
+
if (entry.wantType === WantType.WantHave) {
|
|
123
|
+
if (block.byteLength < this.maxSizeReplaceHasWithBlock) {
|
|
124
|
+
// if the block is small we just send it to them
|
|
125
|
+
sentBlocks.add(key)
|
|
126
|
+
message.blocks.push({
|
|
127
|
+
data: block,
|
|
128
|
+
prefix: cidToPrefix(entry.cid)
|
|
129
|
+
})
|
|
130
|
+
} else {
|
|
131
|
+
// otherwise tell them we have the block
|
|
132
|
+
message.blockPresences.push({
|
|
133
|
+
cid: entry.cid.bytes,
|
|
134
|
+
type: BlockPresenceType.HaveBlock
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// they want the block, send it to them
|
|
139
|
+
sentBlocks.add(key)
|
|
140
|
+
message.blocks.push({
|
|
141
|
+
data: block,
|
|
142
|
+
prefix: cidToPrefix(entry.cid)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// only send the message if we actually have something to send
|
|
148
|
+
if (message.blocks.length > 0 || message.blockPresences.length > 0) {
|
|
149
|
+
await this.network.sendMessage(this.peerId, message, options)
|
|
150
|
+
|
|
151
|
+
// update accounting
|
|
152
|
+
this.sentBytes(message.blocks.reduce((acc, curr) => acc + curr.data.byteLength, 0))
|
|
153
|
+
|
|
154
|
+
// remove sent blocks from local copy of their want list - they can still
|
|
155
|
+
// re-request if required
|
|
156
|
+
for (const key of sentBlocks) {
|
|
157
|
+
this.wants.delete(key)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { CodeError } from '@libp2p/interface'
|
|
2
|
+
import { PeerSet } from '@libp2p/peer-collections'
|
|
3
|
+
import { PeerQueue } from '@libp2p/utils/peer-queue'
|
|
4
|
+
import map from 'it-map'
|
|
5
|
+
import merge from 'it-merge'
|
|
6
|
+
import pDefer, { type DeferredPromise } from 'p-defer'
|
|
7
|
+
import type { BitswapWantProgressEvents, BitswapSession as BitswapSessionInterface } from './index.js'
|
|
8
|
+
import type { Network } from './network.js'
|
|
9
|
+
import type { WantList } from './want-list.js'
|
|
10
|
+
import type { ComponentLogger, Logger, PeerId } from '@libp2p/interface'
|
|
11
|
+
import type { AbortOptions } from 'interface-store'
|
|
12
|
+
import type { CID } from 'multiformats/cid'
|
|
13
|
+
import type { ProgressOptions } from 'progress-events'
|
|
14
|
+
|
|
15
|
+
export interface BitswapSessionComponents {
|
|
16
|
+
network: Network
|
|
17
|
+
wantList: WantList
|
|
18
|
+
logger: ComponentLogger
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BitswapSessionInit extends AbortOptions {
|
|
22
|
+
root: CID
|
|
23
|
+
queryConcurrency: number
|
|
24
|
+
minProviders: number
|
|
25
|
+
maxProviders: number
|
|
26
|
+
connectedPeers: PeerId[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class BitswapSession implements BitswapSessionInterface {
|
|
30
|
+
public readonly root: CID
|
|
31
|
+
public readonly peers: PeerSet
|
|
32
|
+
private readonly log: Logger
|
|
33
|
+
private readonly wantList: WantList
|
|
34
|
+
private readonly network: Network
|
|
35
|
+
private readonly queue: PeerQueue
|
|
36
|
+
private readonly maxProviders: number
|
|
37
|
+
|
|
38
|
+
constructor (components: BitswapSessionComponents, init: BitswapSessionInit) {
|
|
39
|
+
this.peers = new PeerSet()
|
|
40
|
+
this.root = init.root
|
|
41
|
+
this.maxProviders = init.maxProviders
|
|
42
|
+
this.log = components.logger.forComponent(`helia:bitswap:session:${init.root}`)
|
|
43
|
+
this.wantList = components.wantList
|
|
44
|
+
this.network = components.network
|
|
45
|
+
|
|
46
|
+
this.queue = new PeerQueue({
|
|
47
|
+
concurrency: init.queryConcurrency
|
|
48
|
+
})
|
|
49
|
+
this.queue.addEventListener('error', (evt) => {
|
|
50
|
+
this.log.error('error querying peer for %c', this.root, evt.detail)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async want (cid: CID, options: AbortOptions & ProgressOptions<BitswapWantProgressEvents> = {}): Promise<Uint8Array> {
|
|
55
|
+
if (this.peers.size === 0) {
|
|
56
|
+
throw new CodeError('Bitswap session had no peers', 'ERR_NO_SESSION_PEERS')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.log('sending WANT-BLOCK for %c to', cid, this.peers)
|
|
60
|
+
|
|
61
|
+
const result = await Promise.any(
|
|
62
|
+
[...this.peers].map(async peerId => {
|
|
63
|
+
return this.wantList.wantBlock(cid, {
|
|
64
|
+
peerId,
|
|
65
|
+
...options
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
this.log('received block for %c from %p', cid, result.sender)
|
|
71
|
+
|
|
72
|
+
// TODO findNewProviders when promise.any throws aggregate error and signal
|
|
73
|
+
// is not aborted
|
|
74
|
+
|
|
75
|
+
return result.block
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async findNewProviders (cid: CID, count: number, options: AbortOptions = {}): Promise<void> {
|
|
79
|
+
const deferred: DeferredPromise<void> = pDefer()
|
|
80
|
+
let found = 0
|
|
81
|
+
|
|
82
|
+
this.log('find %d-%d new provider(s) for %c', count, this.maxProviders, cid)
|
|
83
|
+
|
|
84
|
+
const source = merge(
|
|
85
|
+
[...this.wantList.peers.keys()],
|
|
86
|
+
map(this.network.findProviders(cid, options), prov => prov.id)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
void Promise.resolve()
|
|
90
|
+
.then(async () => {
|
|
91
|
+
for await (const peerId of source) {
|
|
92
|
+
// eslint-disable-next-line no-loop-func
|
|
93
|
+
await this.queue.add(async () => {
|
|
94
|
+
try {
|
|
95
|
+
this.log('asking potential session peer %p if they have %c', peerId, cid)
|
|
96
|
+
const result = await this.wantList.wantPresence(cid, {
|
|
97
|
+
peerId,
|
|
98
|
+
...options
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (!result.has) {
|
|
102
|
+
this.log('potential session peer %p did not have %c', peerId, cid)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.log('potential session peer %p had %c', peerId, cid)
|
|
107
|
+
found++
|
|
108
|
+
|
|
109
|
+
// add to list
|
|
110
|
+
this.peers.add(peerId)
|
|
111
|
+
|
|
112
|
+
if (found === count) {
|
|
113
|
+
this.log('found %d session peers', found)
|
|
114
|
+
|
|
115
|
+
deferred.resolve()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (found === this.maxProviders) {
|
|
119
|
+
this.log('found max provider session peers', found)
|
|
120
|
+
|
|
121
|
+
this.queue.clear()
|
|
122
|
+
}
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
this.log.error('error querying potential session peer %p for %c', peerId, cid, err.errors ?? err)
|
|
125
|
+
}
|
|
126
|
+
}, {
|
|
127
|
+
peerId
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.log('found %d session peers total', found)
|
|
132
|
+
|
|
133
|
+
if (count > 0) {
|
|
134
|
+
deferred.reject(new CodeError(`Found ${found} of ${count} providers`, 'ERR_NO_PROVIDERS_FOUND'))
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return deferred.promise
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function createBitswapSession (components: BitswapSessionComponents, init: BitswapSessionInit): Promise<BitswapSessionInterface> {
|
|
143
|
+
const session = new BitswapSession(components, init)
|
|
144
|
+
|
|
145
|
+
await session.findNewProviders(init.root, init.minProviders, {
|
|
146
|
+
signal: init.signal
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return session
|
|
150
|
+
}
|
package/src/stats.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MetricGroup, Metrics, PeerId } from '@libp2p/interface'
|
|
2
|
+
|
|
3
|
+
export interface StatsComponents {
|
|
4
|
+
metrics?: Metrics
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class Stats {
|
|
8
|
+
private readonly blocksReceived?: MetricGroup
|
|
9
|
+
private readonly duplicateBlocksReceived?: MetricGroup
|
|
10
|
+
private readonly dataReceived?: MetricGroup
|
|
11
|
+
private readonly duplicateDataReceived?: MetricGroup
|
|
12
|
+
|
|
13
|
+
constructor (components: StatsComponents) {
|
|
14
|
+
this.blocksReceived = components.metrics?.registerMetricGroup('ipfs_bitswap_received_blocks')
|
|
15
|
+
this.duplicateBlocksReceived = components.metrics?.registerMetricGroup('ipfs_bitswap_duplicate_received_blocks')
|
|
16
|
+
this.dataReceived = components.metrics?.registerMetricGroup('ipfs_bitswap_data_received_bytes')
|
|
17
|
+
this.duplicateDataReceived = components.metrics?.registerMetricGroup('ipfs_bitswap_duplicate_data_received_bytes')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
updateBlocksReceived (count: number = 1, peerId?: PeerId): void {
|
|
21
|
+
const stats: Record<string, number | unknown> = {
|
|
22
|
+
global: count
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (peerId != null) {
|
|
26
|
+
stats[peerId.toString()] = count
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.blocksReceived?.increment(stats)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
updateDuplicateBlocksReceived (count: number = 1, peerId?: PeerId): void {
|
|
33
|
+
const stats: Record<string, number | unknown> = {
|
|
34
|
+
global: count
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (peerId != null) {
|
|
38
|
+
stats[peerId.toString()] = count
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.duplicateBlocksReceived?.increment(stats)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
updateDataReceived (bytes: number, peerId?: PeerId): void {
|
|
45
|
+
const stats: Record<string, number> = {
|
|
46
|
+
global: bytes
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (peerId != null) {
|
|
50
|
+
stats[peerId.toString()] = bytes
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.dataReceived?.increment(stats)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
updateDuplicateDataReceived (bytes: number, peerId?: PeerId): void {
|
|
57
|
+
const stats: Record<string, number> = {
|
|
58
|
+
global: bytes
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (peerId != null) {
|
|
62
|
+
stats[peerId.toString()] = bytes
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.duplicateDataReceived?.increment(stats)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { decode, encodingLength } from 'uint8-varint'
|
|
2
|
+
|
|
3
|
+
function varintDecoder (buf: Uint8Array): number[] {
|
|
4
|
+
if (!(buf instanceof Uint8Array)) {
|
|
5
|
+
throw new Error('arg needs to be a Uint8Array')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const result: number[] = []
|
|
9
|
+
|
|
10
|
+
while (buf.length > 0) {
|
|
11
|
+
const num = decode(buf)
|
|
12
|
+
result.push(num)
|
|
13
|
+
buf = buf.slice(encodingLength(num))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return result
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default varintDecoder
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { encode, encodingLength } from 'uint8-varint'
|
|
2
|
+
|
|
3
|
+
function varintEncoder (buf: number[]): Uint8Array {
|
|
4
|
+
let out = new Uint8Array(buf.reduce((acc, curr) => {
|
|
5
|
+
return acc + encodingLength(curr)
|
|
6
|
+
}, 0))
|
|
7
|
+
let offset = 0
|
|
8
|
+
|
|
9
|
+
for (const num of buf) {
|
|
10
|
+
out = encode(num, out, offset)
|
|
11
|
+
|
|
12
|
+
offset += encodingLength(num)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return out
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default varintEncoder
|