resources > Release Notes & Upgrade Guides > Руководство по миграции AssemblyScript

Руководство по миграции AssemblyScript

Reading time: 9 min

До сих пор для субграфов использовалась одна из первых версий AssemblyScript (v0.6). Наконец, мы добавили поддержку последней доступной версии (v0.19.10)! 🎉

Это позволит разработчикам субграфов использовать более новые возможности языка AS и стандартной библиотеки.

Это руководство применимо для всех, кто использует graph-cli/graph-ts версии ниже 0.22.0. Если у Вас уже есть версия выше (или равная) этой, значит, Вы уже использовали версию 0.19.10 AssemblyScript 🙂

Примечание. Начиная с 0.24.0, graph-node может поддерживать обе версии, в зависимости от apiVersion, указанного в манифесте субграфа.

Особенности

Ссылка на этот раздел

Новый функционал

Ссылка на этот раздел
  • Теперь TypedArray можно создавать, используя ArrayBuffer6 с помощью нового статического метода wrap (v0.8.1)
  • Новые функции стандартной библиотеки: String#toUpperCase, String#toLowerCase, String#localeCompare и TypedArray#set (v0.9.0)
  • Добавлена поддержка x instanceof GenericClass (v0.9.2)
  • Добавлен StaticArray<T>, более эффективный вариант массива (v0.9.3)
  • Добавлен Array<T>#flat (v0.10.0)
  • Реализован аргумент radix для Number#toString (v0.10.1)
  • Добавлена поддержка разделителей в литералах с плавающей точкой (v0.13.7)
  • Добавлена поддержка функций первого класса (v0.14.0)
  • Добавление встроенных модулей: i32/i64/f32/f64.add/sub/mul (v0.14.13)
  • Внедрение Array/TypedArray/String#at (v0.18.2)
  • Добавлена поддержка литеральных строк шаблона (v0.18.17)
  • Добавление encodeURI(Component) и decodeURI(Component) (v0.18.27)
  • Добавление toString, toDateString и toTimeString к Date ([v0.18.29](https://github.com/ AssemblyScript/assemblyscript/releases/tag/v0.18.29))
  • Добавление toUTCString для Date (v0.18.30)
  • Добавление встроенного типа nonnull/NonNullable (v0.19.2)
  • Функции Math, такие как exp, exp2, log, log2 и pow были заменены более быстрыми вариантами (v0.9.0)
  • Проведена небольшая оптимизация Math.mod (v0.17.1)
  • Кэширование большего количества обращений к полям в std Map и Set (v0.17.8)
  • Оптимизация по двум степеням в ipow32/64 (v0.18.2)
  • Тип литерала массива теперь можно определить по его содержимому (v0.9.0)
  • Стандартная библиотека обновлена до версии Unicode 13.0.0 (v0.10.0)

Как выполнить обновление?

Ссылка на этот раздел
  1. Измените мэппинги apiVersion в subgraph.yaml на 0.0.6:
...
dataSources:
...
mapping:
...
apiVersion: 0.0.6
...
  1. Обновите используемый Вами graph-cli до latest версии, выполнив:
# если он у Вас установлен глобально
npm install --global @graphprotocol/graph-cli@latest
# или в Вашем субграфе, если он у Вас как зависимость dev
npm install --save-dev @graphprotocol/graph-cli@latest
  1. Сделайте то же самое для graph-ts, но вместо глобальной установки сохраните его в своих основных зависимостях:
npm install --save @graphprotocol/graph-ts@latest
  1. Следуйте остальной части руководства, чтобы исправить языковые изменения.
  2. Снова запустите codegen и deploy.

Критические изменения

Ссылка на этот раздел

Обнуляемость

Ссылка на этот раздел

В более старой версии AssemblyScript можно было создать такой код:

function load(): Value | null { ... }
let maybeValue = load();
maybeValue.aMethod();

Однако в новой версии, поскольку значение обнуляемо, требуется проверка, например, такая:

let maybeValue = load()
if (maybeValue) {
maybeValue.aMethod() // `maybeValue` is not null anymore
}

Или принудительно вот такая:

let maybeValue = load()! // прерывается во время выполнения, если значение равно null
maybeValue.aMethod()

Если Вы не уверены, что выбрать, мы рекомендуем всегда использовать безопасную версию. Если значение не существует, Вы можете просто выполнить раннее выражение if с возвратом в обработчике субграфа.

Затенение переменных

Ссылка на этот раздел

Раньше можно было сделать затенение переменных и код, подобный этому, работал:

let a = 10
let b = 20
let a = a + b

Однако теперь это больше невозможно, и компилятор возвращает эту ошибку:

ERROR TS2451: Cannot redeclare block-scoped variable 'a'
let a = a + b;
~~~~~~~~~~~~~
in assembly/index.ts(4,3)

Вам нужно будет переименовать дублированные переменные, если Вы используете затенение переменных.

Нулевые сравнения

Ссылка на этот раздел

Выполняя обновление своего субграфа, иногда Вы можете получить такие ошибки:

ERROR TS2322: Type '~lib/@graphprotocol/graph-ts/common/numbers/BigInt | null' is not assignable to type '~lib/@graphprotocol/graph-ts/common/numbers/BigInt'.
if (decimals == null) {
~~~~
in src/mappings/file.ts(41,21)

Чтобы решить эту проблему, Вы можете просто изменить оператор if на что-то вроде этого:

if (!decimals) {
// or
if (decimals === null) {

Подобное относится к случаям, когда вместо == используется !=.

Раньше для кастинга обычно использовалось ключевое слово as, например:

let byteArray = new ByteArray(10)
let uint8Array = byteArray as Uint8Array // equivalent to: <Uint8Array>byteArray

Однако это работает только в двух случаях:

  • Примитивный кастинг (между такими типами, как u8, i32, bool; например: let b: isize = 10; b as usize);
  • Укрупнение по наследованию классов (subclass → superclass)

Примеры:

// примитивный кастинг
let a: usize = 10
let b: isize = 5
let c: usize = a + (b as usize)
// укрупнение по наследованию классов
class Bytes extends Uint8Array {}
let bytes = new Bytes(2)
// <Uint8Array>bytes // то же, что: bytes as Uint8Array

Есть два сценария, в которых Вы можете захотеть выполнить преобразование, но использовать as/<T>var небезопасно:

  • Понижение уровня наследования классов (superclass → subclass)
  • Между двумя типами, имеющими общий супер класс
// понижение уровня наследования классов
class Bytes extends Uint8Array {}
let uint8Array = new Uint8Array(2)
// <Bytes>uint8Array // перерывы в работе :(
// между двумя типами, имеющими общий суперкласс
class Bytes extends Uint8Array {}
class ByteArray extends Uint8Array {}
let bytes = new Bytes(2)
// <ByteArray>bytes // перерывы в работе :(

В таких случаях можно использовать функцию changetype<T>:

// понижение уровня наследования классов
class Bytes extends Uint8Array {}
let uint8Array = new Uint8Array(2)
changetype<Bytes>(uint8Array) // работает :)
// между двумя типами, имеющими общий суперкласс
class Bytes extends Uint8Array {}
class ByteArray extends Uint8Array {}
let bytes = new Bytes(2)
changetype<ByteArray>(bytes) // работает :)

Если Вы просто хотите удалить значение NULL, Вы можете продолжать использовать оператор as (или <T>variable), но помните, что значение не может быть нулевым, иначе оно сломается.

// удалить значение NULL
let previousBalance = AccountBalance.load(balanceId) // AccountBalance | null
if (previousBalance != null) {
return previousBalance as AccountBalance // безопасно удалить значение NULL
}
let newBalance = new AccountBalance(balanceId)

В случае обнуления мы рекомендуем Вам обратить внимание на функцию проверки обнуления, это сделает ваш код чище 🙂

Также мы добавили еще несколько статических методов в некоторые типы, чтобы облегчить кастинг:

  • Bytes.fromByteArray
  • Bytes.fromUint8Array
  • BigInt.fromByteArray
  • ByteArray.fromBigInt

Проверка нулевого значения с доступом к свойству

Ссылка на этот раздел

Чтобы применить функцию проверки на нулевое значение, Вы можете использовать операторы if или тернарный оператор (? и :) следующим образом:

let something: string | null = 'data'
let somethingOrElse = something ? something : 'else'
// или
let somethingOrElse
if (something) {
somethingOrElse = something
} else {
somethingOrElse = 'else'
}

Однако это работает только тогда, когда Вы выполняете if / тернарную операцию для переменной, а не для доступа к свойству, например:

class Container {
data: string | null
}
let container = new Container()
container.data = 'data'
let somethingOrElse: string = container.data ? container.data : 'else' // не компилируется

В результате чего выдается ошибка:

ERROR TS2322: Type '~lib/string/String | null' is not assignable to type '~lib/string/String'.
let somethingOrElse: string = container.data ? container.data : "else";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Чтобы решить эту проблему, Вы можете создать переменную для доступа к этому свойству, чтобы компилятор мог выполнять проверку допустимости значений NULL:

class Container {
data: string | null
}
let container = new Container()
container.data = 'data'
let data = container.data
let somethingOrElse: string = data ? data : 'else' // компилируется просто отлично :)

Перегрузка оператора при доступе к свойствам

Ссылка на этот раздел

Если Вы попытаетесь суммировать (например) тип, допускающий значение Null (из доступа к свойству), с типом, не допускающим значение Null, компилятор AssemblyScript вместо того, чтобы выдать предупреждение об ошибке компиляции, предупреждающую, что одно из значений допускает значение Null, просто компилируется молча, давая возможность сломать код во время выполнения.

class BigInt extends Uint8Array {
@operator('+')
plus(other: BigInt): BigInt {
// ...
}
}
class Wrapper {
public constructor(public n: BigInt | null) {}
}
let x = BigInt.fromI32(2)
let y: BigInt | null = null
x + y // выдает ошибку времени компиляции о возможности обнуления
let wrapper = new Wrapper(y)
wrapper.n = wrapper.n + x // не выдает ошибок времени компиляции, как это должно быть

Мы открыли вопрос по этому поводу для компилятора AssemblyScript, но пока, если Вы выполняете подобные операции в своих мэппингах субграфов, Вам следует изменить их так, чтобы перед этим выполнялась проверка на нулевое значение.

let wrapper = new Wrapper(y)
if (!wrapper.n) {
wrapper.n = BigInt.fromI32(0)
}
wrapper.n = wrapper.n + x // теперь `n` гарантированно будет BigInt

Инициализация значения

Ссылка на этот раздел

Если у Вас есть такой код:

var value: Type // null
value.x = 10
value.y = 'content'

Он будет скомпилирован, но сломается во время выполнения. Это происходит из-за того, что значение не было инициализировано, поэтому убедитесь, что Ваш субграф инициализировал свои значения, например так:

var value = new Type() // initialized
value.x = 10
value.y = 'content'

Также, если у Вас есть свойства, допускающие значение NULL, в объекте GraphQL, например:

type Total @entity {
id: Bytes!
amount: BigInt
}

И у Вас есть код, аналогичный этому:

let total = Total.load('latest')
if (total === null) {
total = new Total('latest')
}
total.amount = total.amount + BigInt.fromI32(1)

Вам необходимо убедиться, что значение total.amount инициализировано, потому что, если Вы попытаетесь получить доступ к сумме, как в последней строке, произойдет сбой. Таким образом, Вы либо инициализируете его первым:

let total = Total.load('latest')
if (total === null) {
total = new Total('latest')
total.amount = BigInt.fromI32(0)
}
total.tokens = total.tokens + BigInt.fromI32(1)

Или Вы можете просто изменить свою схему GraphQL, чтобы не использовать тип, допускающий значение NULL для этого свойства. Тогда мы инициализируем его нулем на этапе codegen 😉

type Total @entity {
id: Bytes!
amount: BigInt!
}
let total = Total.load('latest')
if (total === null) {
total = new Total('latest') // уже инициализирует свойства, не допускающие значения NULL
}
total.amount = total.amount + BigInt.fromI32(1)

Инициализация свойств класса

Ссылка на этот раздел

Если Вы экспортируете какие-либо классы со свойствами, которые являются другими классами (декларированными Вами или стандартной библиотекой), то это выглядит следующим образом:

class Thing {}
export class Something {
value: Thing
}

Компилятор выдаст ошибку, потому что Вам нужно либо добавить инициализатор для свойств, являющихся классами, либо добавить оператор !:

export class Something {
constructor(public value: Thing) {}
}
// или
export class Something {
value: Thing
constructor(value: Thing) {
this.value = value
}
}
// или
export class Something {
value!: Thing
}

Инициализация массива

Ссылка на этот раздел

Класс Array по-прежнему принимает число для инициализации длины списка, однако Вам следует соблюдать осторожность, поскольку такие операции, как .push, фактически увеличивают размер, а не добавляют его в начало, например:

let arr = new Array<string>(5) // ["", "", "", "", ""]
arr.push('something') // ["", "", "", "", "", "something"] // size 6 :(

В зависимости от используемых типов, например, допускающих значение NULL, и способа доступа к ним, можно столкнуться с ошибкой времени выполнения, подобной этой:

ERRO Handler skipped due to execution failure, error: Mapping aborted at ~lib/array.ts, line 110, column 40, with message: Element type must be nullable if array is holey wasm backtrace: 0: 0x19c4 - <unknown>!~lib/@graphprotocol/graph-ts/index/format 1: 0x1e75 - <unknown>!~lib/@graphprotocol/graph-ts/common/collections/Entity#constructor 2: 0x30b9 - <unknown>!node_modules/@graphprotocol/graph-ts/global/global/id_of_type

Для того чтобы фактически начать, Вы должны либо инициализировать Array нулевым размером, следующим образом:

let arr = new Array<string>(0) // []
arr.push('something') // ["something"]

Или Вы должны изменить его через индекс:

let arr = new Array<string>(5) // ["", "", "", "", ""]
arr[0] = 'something' // ["something", "", "", "", ""]

Это не прямое изменение AssemblyScript, но Вам, возможно, придется обновить файл schema.graphql.

Теперь Вы больше не можете определять поля в своих типах, которые являются списками, не допускающими значение NULL. Если у Вас такая схема:

type Something @entity {
id: Bytes!
}
type MyEntity @entity {
id: Bytes!
invalidField: [Something]! # no longer valid
}

Вам нужно добавить ! к элементу типа List, например, так:

type Something @entity {
id: Bytes!
}
type MyEntity @entity {
id: Bytes!
invalidField: [Something!]! # valid
}

Изменение произошло из-за различий в допустимости значений NULL между версиями AssemblyScript и связано с файлом src/generated/schema.ts (путь по умолчанию, возможно, Вы его изменили).

  • Map#set и Set#add согласованы со спецификацией, произведён возврат к this (v0.9.2)
  • Массивы больше не наследуются от ArrayBufferView, а являются самостоятельными (v0.10.0)
  • Классы, инициализируемые из объектных литералов, больше не могут определять конструктор (v0.10.0)
  • Результатом бинарной операции ** теперь является целое число с общим знаменателем, если оба операнда являются целыми числами. Раньше результатом было число с плавающей запятой, как при вызове Math/f.pow (v0.11.0)
  • Приведение NaN к false при преобразовании в bool (v0.14.9)
  • При сдвиге небольшого целочисленного значения типа i8/u8 или i16/u16, на результат влияют только соответственно 3 или 4 младших разряда значения RHS, аналогично тому, как при сдвиге i32.shl на результат влияют только 5 младших разрядов значения RHS. Пример: someI8 << 8 ранее выдавал значение 0, но теперь выдает значение someI8 благодаря маскировке RHS как 8 & 7 = 0 (3 бита) (v0.17.0)
  • Исправлена ошибка сравнения реляционных строк при разных размерах (v0.17.8)
Редактировать страницу

Предыдущий
Кураторство
Следующий
Руководство по переходу на валидацию GraphQL
Редактировать страницу