Spec-Zone.ru › Deno 2

Deno Namespace APIs

Глобальное Deno пространство имён содержит API, которые не являются веб-стандартами, включая API для чтения файлов, открытия TCP-сокет, предоставления HTTP и выполнения подпроцессов и т. д.

Изучить все API Deno

Ниже мы выделим некоторые из самых важных API Deno.

Система файлов

Интерпретатор Deno поставляется с различными функциями для работы с файлами и каталогами. Вам потребуется использовать разрешения --allow-read и --allow-write, чтобы получить доступ к файловой системе.

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

  • Чтение файлов различными способами
  • Чтение файлов потоками
  • Чтение текстового файла (Deno.readTextFile)
  • Запись текстового файла (Deno.writeTextFile)

Сеть

Интерпретатор Deno поставляется с встроенными функциями для работы с подключениями к сетевым портам.

Обратитесь к ссылкам ниже для получения примеров кода для общих функций.

  • Подключение к хосту и порту (Deno.connect)
  • Объявление на локальном транспортном адресе (Deno.listen)

Подпроцессы

Интерпретатор Deno поставляется с встроенными функциями для запуска подпроцессов.

Обратитесь к ссылкам ниже для примеров кода, как создать подпроцесс.

  • Создание подпроцесса (Deno.Command)

Ошибки

Интерпретатор Deno поставляется с 20 классами ошибок, которые могут быть подняты в ответ на ряд условий.

Вот некоторые примеры:

Deno.errors.NotFound;
Deno.errors.WriteZero;

Они могут быть использованы следующим образом:

try {
  const file = await Deno.open("./some/file.txt");
} catch (error) {
  if (error instanceof Deno.errors.NotFound) {
    console.error("the file was not found");
  } else {
    // otherwise re-throw
    throw error;
  }
}

HTTP-сервер

Deno имеет два API HTTP-серверов:

  • Deno.serve: нативный, высокоуровневый, поддерживает HTTP/1.1 и HTTP2, это предпочтительный API для написания HTTP-серверов в Deno.
  • Deno.serveHttp: нативный, низкоуровневый, поддерживает HTTP/1.1 и HTTP2.

Для запуска HTTP-сервера на заданном порту используйте функцию Deno.serve. Эта функция принимает функцию-обработчик, которая будет вызываться для каждого входящего запроса и должна возвращать ответ (или промис, разрешающий ответ). Например:

Deno.serve((_req) => {
  return new Response("Hello, World!");
});

По умолчанию Deno.serve будет слушать порт 8000, но это можно изменить, передав номер порта в пакете опций в качестве первого или второго аргумента.

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

Разрешения

Разрешения предоставляются из командной строки при запуске команды deno. Код пользователя часто предполагает свой собственный набор необходимых разрешений, но нет гарантии во время выполнения, что набор предоставленных разрешений будет совпадать с этим.

В некоторых случаях для обеспечения отказоустойчивости программы требуется способ взаимодействия с системой разрешений во время выполнения.

Дескрипторы разрешений

В командной строке разрешение на чтение для /foo/bar представлено как --allow-read=/foo/bar. В JavaScript во время выполнения оно представлено следующим образом:

const desc = { name: "read", path: "/foo/bar" } as const;

Другие примеры:

// Global write permission.
const desc1 = { name: "write" } as const;

// Write permission to `$PWD/foo/bar`.
const desc2 = { name: "write", path: "foo/bar" } as const;

// Global net permission.
const desc3 = { name: "net" } as const;

// Net permission to 127.0.0.1:8000.
const desc4 = { name: "net", host: "127.0.0.1:8000" } as const;

// High-resolution time permission.
const desc5 = { name: "hrtime" } as const;

См. PermissionDescriptor в справочнике API для получения дополнительной информации. Синхронные API-аналоги (например, Deno.permissions.querySync) существуют для всех описанных ниже API.

Запрос разрешений

Проверьте, предоставлено ли разрешение по описателю или нет.

// deno run --allow-read=/foo main.ts

const desc1 = { name: "read", path: "/foo" } as const;
console.log(await Deno.permissions.query(desc1));
// PermissionStatus { state: "granted", partial: false }

const desc2 = { name: "read", path: "/foo/bar" } as const;
console.log(await Deno.permissions.query(desc2));
// PermissionStatus { state: "granted", partial: false }

const desc3 = { name: "read", path: "/bar" } as const;
console.log(await Deno.permissions.query(desc3));
// PermissionStatus { state: "prompt", partial: false }

Если флаг --deny-read был использован для ограничения некоторых путей к файлам, результат будет содержать partial: true, указывающее, что не всем подпутям предоставлены разрешения:

// deno run --allow-read=/foo --deny-read=/foo/bar main.ts

const desc1 = { name: "read", path: "/foo" } as const;
console.log(await Deno.permissions.query(desc1));
// PermissionStatus { state: "granted", partial: true }

const desc2 = { name: "read", path: "/foo/bar" } as const;
console.log(await Deno.permissions.query(desc2));
// PermissionStatus { state: "denied", partial: false }

const desc3 = { name: "read", path: "/bar" } as const;
console.log(await Deno.permissions.query(desc3));
// PermissionStatus { state: "prompt", partial: false }

Состояния разрешений

Состояние разрешения может быть «предоставлено», «запрошено» или «запрещено». Разрешения, которые были предоставлены из командной строки, будут запрашиваться как { state: "granted" }. Те, которые не были предоставлены, запрашиваются как { state: "prompt" } по умолчанию, в то время как { state: "denied" } предназначены для тех, которые были явно запрещены. Это будет появляться в Запросе разрешений.

Сила разрешений

Интуитивное понимание результата второго запроса в Запросе разрешений заключается в том, что доступ на чтение был предоставлен для /foo, и /foo/bar находится внутри /foo, поэтому /foo/bar может быть прочитан. Это верно, если предоставленное из командной строки разрешение не является *частичным* запрошенным разрешениям (в результате использования флага --deny-*).

Мы также можем сказать, что desc1 *менее строгий, чем* desc2. Это означает, что для любого набора разрешений, предоставленных из командной строки:

  1. Если desc1 запрашивает { state: "granted", partial: false }, то так же должно запрашиваться desc2.
  2. Если desc2 запрашивает { state: "denied", partial: false }, то так же должно запрашиваться desc1.

Дополнительные примеры:

const desc1 = { name: "write" } as const;
// is stronger than
const desc2 = { name: "write", path: "/foo" } as const;

const desc3 = { name: "net", host: "127.0.0.1" } as const;
// is stronger than
const desc4 = { name: "net", host: "127.0.0.1:8000" } as const;

Запрос разрешений

Запросите не предоставленное разрешение у пользователя через запрос в командной строке.

// deno run main.ts

const desc1 = { name: "read", path: "/foo" } as const;
const status1 = await Deno.permissions.request(desc1);
// ⚠️ Deno requests read access to "/foo". Grant? [y/n (y = yes allow, n = no deny)] y
console.log(status1);
// PermissionStatus { state: "granted", partial: false }

const desc2 = { name: "read", path: "/bar" } as const;
const status2 = await Deno.permissions.request(desc2);
// ⚠️ Deno requests read access to "/bar". Grant? [y/n (y = yes allow, n = no deny)] n
console.log(status2);
// PermissionStatus { state: "denied", partial: false }

Если текущее состояние разрешения равно «запрос», на терминале пользователя появится запрос, спрашивающий, хочет ли он предоставить запрос. Запрос на desc1 был предоставлен, поэтому его новое состояние возвращается, и выполнение будет продолжено так, как если бы --allow-read=/foo было указано в командной строке. Запрос на desc2 был отклонен, поэтому его состояние разрешения понижено с «запрос» до «отказ».

Если текущее состояние разрешения уже равно «предоставлено» или «запрещено», запрос будет работать как запрос и просто вернет текущее состояние. Это предотвращает запросы как для уже предоставленных, так и для ранее отклоненных запросов.

Отзыв разрешений

Понизить разрешение с «предоставлено» до «запрос».

// deno run --allow-read=/foo main.ts

const desc = { name: "read", path: "/foo" } as const;
console.log(await Deno.permissions.revoke(desc));
// PermissionStatus { state: "prompt", partial: false }

Что происходит, когда вы пытаетесь отозвать разрешение, которое является *частичным* к одному, предоставленному из командной строки?

// deno run --allow-read=/foo main.ts

const desc = { name: "read", path: "/foo/bar" } as const;
console.log(await Deno.permissions.revoke(desc));
// PermissionStatus { state: "prompt", partial: false }
const cliDesc = { name: "read", path: "/foo" } as const;
console.log(await Deno.permissions.revoke(cliDesc));
// PermissionStatus { state: "prompt", partial: false }

Предоставленное из командной строки разрешение, которое подразумевает отозванное разрешение, также было отозвано.

Чтобы понять это поведение, представьте, что Deno хранит внутренний набор *явно предоставленных дескрипторов разрешений*. Указание --allow-read=/foo,/bar в командной строке инициализирует этот набор следующим образом:

[
  { name: "read", path: "/foo" },
  { name: "read", path: "/bar" },
];

Предоставление запроса во время выполнения для { name: "write", path: "/foo" } обновляет набор следующим образом:

[
  { name: "read", path: "/foo" },
  { name: "read", path: "/bar" },
  { name: "write", path: "/foo" },
];

Алгоритм отзыва разрешений Deno работает путем удаления каждого элемента из этого набора, который *более строгий, чем* дескриптор разрешения аргумента.

Deno не допускает «фрагментированных» состояний разрешений, где некоторое сильное разрешение предоставляется с исключениями слабых разрешений, подразумеваемых им. Такая система окажется все более сложной и непредсказуемой по мере учета более широкого спектра вариантов использования и состояния "denied". Это выверенный компромисс между гранулярностью и безопасностью.

import.meta

Deno поддерживает ряд свойств и методов в API import.meta. Его можно использовать для получения информации о модуле, такой как URL модуля.

import.meta.url

Возвращает URL текущего модуля.

main.ts
console.log(import.meta.url);
$ deno run main.ts
file:///dev/main.ts

$ deno run https:/example.com/main.ts
https://example.com/main.ts

import.meta.main

Возвращает, является ли текущий модуль точкой входа в вашу программу.

main.ts
import "./other.ts";

console.log(`Is ${import.meta.url} the main module?`, import.meta.main);
other.ts
console.log(`Is ${import.meta.url} the main module?`, import.meta.main);
$ deno run main.ts
Is file:///dev/other.ts the main module? false
Is file:///dev/main.ts the main module? true

import.meta.filename

Это свойство доступно только для локальных модулей (модули, имеющие file:///... спецификатор) и возвращает undefined для удалённых модулей.

Возвращает полное разрешённый путь к текущему модулю. Значение содержит разделители путей, специфичные для ОС.

main.ts
console.log(import.meta.filename);

В Unix:

$ deno run main.ts
/dev/main.ts

$ deno run https://example.com/main.ts
undefined

В Windows:

$ deno run main.ts
C:\dev\main.ts

$ deno run https://example.com/main.ts
undefined

import.meta.dirname

Это свойство доступно только для локальных модулей (модули, имеющие file:///... спецификатор) и возвращает undefined для удалённых модулей.

Возвращает полное разрешённый путь к каталогу, содержащему текущий модуль. Значение содержит разделители путей, специфичные для ОС.

main.ts
console.log(import.meta.dirname);

В Unix:

$ deno run main.ts
/dev/

$ deno run https://example.com/main.ts
undefined

В Windows:

$ deno run main.ts
C:\dev\

$ deno run https://example.com/main.ts
undefined

import.meta.resolve

Разрешение спецификаторов относительно текущего модуля.

const worker = new Worker(import.meta.resolve("./worker.ts"));

API import.meta.resolve учитывает текущую применённую карту импорта, что даёт вам возможность разрешать «голые» спецификаторы.

При такой загруженной карте импорта...

{
  "imports": {
    "fresh": "https://deno.land/x/fresh@1.0.1/dev.ts"
  }
}

...вы можете теперь разрешить:

resolve.js
console.log(import.meta.resolve("fresh"));
$ deno run resolve.js
https://deno.land/x/fresh@1.0.1/dev.ts

FFI

API FFI (интерфейс внешних функций) позволяет пользователям вызывать библиотеки, написанные на родных языках, поддерживающих C ABIs (C/C++, Rust, Zig, V и т. д.), используя Deno.dlopen.

Вот пример, демонстрирующий, как вызвать функцию Rust из Deno:

// add.rs
#[no_mangle]
pub extern "C" fn add(a: isize, b: isize) -> isize {
    a + b
}

Компилируйте его в динамическую библиотеку C (libadd.so в Linux):

rustc --crate-type cdylib add.rs

В C вы можете написать это так:

// add.c
int add(int a, int b) {
  return a + b;
}

И скомпилировать его:

// unix
cc -c -o add.o add.c
cc -shared -W -o libadd.so add.o
// Windows
cl /LD add.c /link /EXPORT:add

Вызов библиотеки из Deno:

// ffi.ts

// Determine library extension based on
// your OS.
let libSuffix = "";
switch (Deno.build.os) {
  case "windows":
    libSuffix = "dll";
    break;
  case "darwin":
    libSuffix = "dylib";
    break;
  default:
    libSuffix = "so";
    break;
}

const libName = `./libadd.${libSuffix}`;
// Open library and define exported symbols
const dylib = Deno.dlopen(
  libName,
  {
    "add": { parameters: ["isize", "isize"], result: "isize" },
  } as const,
);

// Call the symbol `add`
const result = dylib.symbols.add(35, 34); // 69

console.log(`Result from external addition of 35 and 34: ${result}`);

Запустите с флагом --allow-ffi и флагом --unstable:

deno run --allow-ffi --unstable ffi.ts

Асинхронное FFI

Существует много случаев, когда пользователям может потребоваться запускать ресурсоемкие функции FFI в фоновом режиме, не блокируя другие задачи в главном потоке.

Начиная с Deno 1.15, символы могут быть помечены nonblocking в Deno.dlopen. Эти вызовы функций будут выполняться в выделенном блокирующем потоке и вернут Promise, разрешающее желаемое result.

Пример выполнения дорогостоящих вызовов FFI с помощью Deno:

// sleep.c
#ifdef _WIN32
#include <Windows.h>
#else
#include <time.h>
#endif

int sleep(unsigned int ms) {
  #ifdef _WIN32
  Sleep(ms);
  #else
  struct timespec ts;
  ts.tv_sec = ms / 1000;
  ts.tv_nsec = (ms % 1000) * 1000000;
  nanosleep(&ts, NULL);
  #endif
}

Вызов из Deno:

// nonblocking_ffi.ts
const library = Deno.dlopen(
  "./sleep.so",
  {
    sleep: {
      parameters: ["usize"],
      result: "void",
      nonblocking: true,
    },
  } as const,
);

library.symbols.sleep(500).then(() => console.log("After"));
console.log("Before");

Результат:

$ deno run --allow-ffi --unstable unblocking_ffi.ts
Before
After

Обратные вызовы

API Deno FFI поддерживает создание обратных вызовов C из функций JavaScript для вызова обратных вызовов в Deno из динамических библиотек. Пример создания и использования обратных вызовов приведен ниже:

// callback_ffi.ts
const library = Deno.dlopen(
  "./callback.so",
  {
    set_status_callback: {
      parameters: ["function"],
      result: "void",
    },
    start_long_operation: {
      parameters: [],
      result: "void",
    },
    check_status: {
      parameters: [],
      result: "void",
    },
  } as const,
);

const callback = new Deno.UnsafeCallback(
  {
    parameters: ["u8"],
    result: "void",
  } as const,
  (success: number) => {},
);

// Pass the callback pointer to dynamic library
library.symbols.set_status_callback(callback.pointer);
// Start some long operation that does not block the thread
library.symbols.start_long_operation();

// Later, trigger the library to check if the operation is done.
// If it is, this call will trigger the callback.
library.symbols.check_status();

Если функция обратного вызова UnsafeCallback вызывает ошибку, ошибка будет передана функции, которая вызвала обратный вызов (выше это check_status()) и может быть перехвачена там. Если обратный вызов, возвращающий значение, вызывает ошибку, Deno вернет 0 (нулевой указатель для указателей) как результат.

UnsafeCallback по умолчанию не освобождается, так как это может вызвать ошибки использования после освобождения. Чтобы правильно утилизировать UnsafeCallback, необходимо вызвать метод close().

const callback = new Deno.UnsafeCallback(
  { parameters: [], result: "void" } as const,
  () => {},
);

// After callback is no longer needed
callback.close();
// It is no longer safe to pass the callback as a parameter.

Также возможно, чтобы родные библиотеки настраивали обработчики прерываний и непосредственно вызывали обратный вызов. Однако это не рекомендуется и может привести к непредвиденным побочным эффектам и неопределенному поведению. В идеале любые обработчики прерываний должны устанавливать только флаг, который позже можно опросить аналогично тому, как используется check_status() выше.

Поддерживаемые типы

Вот список типов, в настоящее время поддерживаемых API Deno FFI.

Тип FFI Deno C Rust
i8 number char / signed char i8
u8 number unsigned char u8
i16 number short int i16
u16 number unsigned short int u16
i32 number int / signed int i32
u32 number unsigned int u32
i64 number | bigint long long int i64
u64 number | bigint unsigned long long int u64
usize number | bigint size_t usize
isize number | bigint size_t isize
f32 number | bigint float f32
f64 number | bigint double f64
void[1] undefined void ()
pointer {} | null void * *mut c_void
buffer[2] TypedArray | null uint8_t * *mut u8
function[3] {} | null void (*fun)() Option<extern "C" fn()>
{ struct: [...] }[4] TypedArray struct MyStruct MyStruct

Начиная с Deno 1.25, тип pointer был разделен на тип pointer и тип buffer для обеспечения оптимизации для типизированных массивов, а начиная с Deno 1.31, JavaScript-представление pointer стало объектом неявного указателя или null для нулевых указателей.

  • [1] Тип void может использоваться только в качестве типа результата.
  • [2] Тип buffer принимает TypedArrays в качестве параметра, но всегда возвращает объект указателя или null, когда используется в качестве типа результата, как тип pointer.
  • [3] Тип function работает точно так же, как тип pointer как параметр и тип результата.
  • [4] Тип struct предназначен для передачи и возврата структур C по значению (копия). Массив struct должен перечислить тип каждого поля структуры в порядке. Структуры автоматически заполняются: упакованные структуры можно определить, используя необходимое количество полей u8 для избежания заполнения. Поддерживаются только TypedArrays, и структуры всегда возвращаются как Uint8Array.

deno_bindgen

deno_bindgen — официальный инструмент для упрощения генерации связующего кода для библиотек Deno FFI, написанных на Rust.

Он аналогичен wasm-bindgen в экосистеме Rust Wasm.

Вот пример его использования:

// mul.rs
use deno_bindgen::deno_bindgen;

#[deno_bindgen]
struct Input {
  a: i32,
  b: i32,
}

#[deno_bindgen]
fn mul(input: Input) -> i32 {
  input.a * input.b
}

Запустите deno_bindgen для генерации связей. Теперь вы можете напрямую импортировать их в Deno:

// mul.ts
import { mul } from "./bindings/bindings.ts";
mul({ a: 10, b: 2 }); // 20

Любые проблемы, связанные с deno_bindgen, следует сообщать по адресу https://github.com/denoland/deno_bindgen/issues

Жизненный цикл программы

Deno поддерживает события жизненного цикла, совместимые с браузером:

  • load: срабатывает, когда вся страница загружена, включая все зависимые ресурсы, такие как таблицы стилей и изображения.
  • beforeunload: срабатывает, когда цикл событий больше не имеет работы и собирается завершиться. Планирование дополнительной асинхронной работы (таких как таймеры или сетевые запросы) заставит программу продолжить работу.
  • unload: срабатывает, когда документ или дочерний ресурс загружаются.
  • unhandledrejection: срабатывает, когда обещание без обработчика отклонения отклоняется, то есть обещание без обработчика .catch() или второго аргумента .then().
  • rejectionhandled: срабатывает, когда обработчик .catch() добавляется к обещанию, которое уже отклонено. Это событие срабатывает только в том случае, если установлен обработчик unhandledrejection слушатель, который предотвращает распространение события (что приведет к завершению программы с ошибкой).

Вы можете использовать эти события для предоставления кода подготовки и завершения в вашей программе.

Обработчики событий load могут быть асинхронными и будут ожидаться, это событие нельзя отменить. Обработчики событий beforeunload должны быть синхронными и могут быть отменены для продолжения работы программы. Обработчики событий unload должны быть синхронными и не могут быть отменены.

main.ts

main.ts
import "./imported.ts";

const handler = (e: Event): void => {
  console.log(`got ${e.type} event in event handler (main)`);
};

globalThis.addEventListener("load", handler);

globalThis.addEventListener("beforeunload", handler);

globalThis.addEventListener("unload", handler);

globalThis.onload = (e: Event): void => {
  console.log(`got ${e.type} event in onload function (main)`);
};

globalThis.onbeforeunload = (e: Event): void => {
  console.log(`got ${e.type} event in onbeforeunload function (main)`);
};

globalThis.onunload = (e: Event): void => {
  console.log(`got ${e.type} event in onunload function (main)`);
};

console.log("log from main script");
imported.ts
const handler = (e: Event): void => {
  console.log(`got ${e.type} event in event handler (imported)`);
};

globalThis.addEventListener("load", handler);
globalThis.addEventListener("beforeunload", handler);
globalThis.addEventListener("unload", handler);

globalThis.onload = (e: Event): void => {
  console.log(`got ${e.type} event in onload function (imported)`);
};

globalThis.onbeforeunload = (e: Event): void => {
  console.log(`got ${e.type} event in onbeforeunload function (imported)`);
};

globalThis.onunload = (e: Event): void => {
  console.log(`got ${e.type} event in onunload function (imported)`);
};

console.log("log from imported script");

Несколько замечаний по этому примеру:

  • addEventListener и onload/onunload имеют префикс globalThis, но вы также можете использовать self или вообще без префикса. Не рекомендуется использовать window в качестве префикса.
  • Вы можете использовать addEventListener и/или onload/onunload для определения обработчиков событий. Между ними существует существенная разница, давайте запустим пример:
$ deno run main.ts
log from imported script
log from main script
got load event in event handler (imported)
got load event in event handler (main)
got load event in onload function (main)
got onbeforeunload event in event handler (imported)
got onbeforeunload event in event handler (main)
got onbeforeunload event in onbeforeunload function (main)
got unload event in event handler (imported)
got unload event in event handler (main)
got unload event in onunload function (main)

Все обработчики, добавленные с помощью addEventListener, были выполнены, но onload, onbeforeunload и onunload, определенные в main.ts, перезаписали обработчики, определенные в imported.ts.

Другими словами, вы можете использовать addEventListener для регистрации нескольких обработчиков событий "load" или "unload", но будут выполнены только последние определенные обработчики событий onload, onbeforeunload, onunload . По этой причине предпочтительно использовать addEventListener при возможности.

beforeunload

// beforeunload.js
let count = 0;

console.log(count);

globalThis.addEventListener("beforeunload", (e) => {
  console.log("About to exit...");
  if (count < 4) {
    e.preventDefault();
    console.log("Scheduling more work...");
    setTimeout(() => {
      console.log(count);
    }, 100);
  }

  count++;
});

globalThis.addEventListener("unload", (e) => {
  console.log("Exiting");
});

count++;
console.log(count);

setTimeout(() => {
  count++;
  console.log(count);
}, 100);

При запуске этой программы будет выведено:

$ deno run beforeunload.js
0
1
2
About to exit...
Scheduling more work...
3
About to exit...
Scheduling more work...
4
About to exit...
Exiting

Событие unhandledrejection

Это событие срабатывает, когда промис, у которого нет обработчика отмены (reject), отклоняется, т. е. промис, у которого нет обработчика .catch() или второго аргумента .then().

// unhandledrejection.js
globalThis.addEventListener("unhandledrejection", (e) => {
  console.log("unhandled rejection at:", e.promise, "reason:", e.reason);
  e.preventDefault();
});

function Foo() {
  this.bar = Promise.reject(new Error("bar not available"));
}

new Foo();
Promise.reject();

Запуск этой программы выведет:

$ deno run unhandledrejection.js
unhandled rejection at: Promise {
  <rejected> Error: bar not available
    at new Foo (file:///dev/unhandled_rejection.js:7:29)
    at file:///dev/unhandled_rejection.js:10:1
} reason: Error: bar not available
    at new Foo (file:///dev/unhandled_rejection.js:7:29)
    at file:///dev/unhandled_rejection.js:10:1
unhandled rejection at: Promise { <rejected> undefined } reason: undefined

© 2018–2024 the Deno authors
Licensed under the MIT License.
https://docs.deno.com/runtime/reference/deno_namespace_apis

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API