@1337exp/2pack 0.0.0-stage → 2.0.0-dev.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## 2.0.0-dev.0
4
+
5
+ A rewrite from scratch; not compatible with 1.x.
6
+
7
+ - Schema as TypeScript values (`define({ hp: i16 })`), types inferred from the schema.
8
+ - Byte format specified in `docs/spec.md`, with test vectors. Fields are written in schema order, little-endian, varint lengths.
9
+ - Three ways to pack with explicit ownership: `pack` (fresh copy), `packInto` (your buffer), `packShared` (view of the codec's buffer).
10
+ - Declared maximum for every string, byte array and list; values over it are errors, never truncated. Integers are checked, not wrapped.
11
+ - Decoding is bounds-checked and throws only `PackError`, with a `code` and a `path`.
12
+ - New types: `bool`, `varuint`, `varint`, `optional`, `array`, `enumOf`, `quant`, nested `define`.
13
+ - Schema evolution by appending: `added(type, default)` fields at the end of a top-level record, and `define(shape, { extensible: true })` so that a first version already ignores what a later one appends. Strict stays the default: only an explicit `extensible: true` accepts bytes after the last known field, `added` alone does not. Zero bytes on the wire, no measurable speed cost, rules in `docs/spec.md`.
14
+ - Zero dependencies, ESM only, no code generation, no build step beyond `tsc`.
15
+ - Removed: runtime string schemas, the `offset` setter, `json<N>`, the browser Buffer polyfill, the UMD bundle.
16
+
17
+ Fixed before the first release, after an independent review of the first commit (each has a regression test in `tests/regressions.test.ts`):
18
+
19
+ - `packInto` of a short string whose declared maximum was 128 or more wrote 1 to 4 stray bytes after the packet, and refused a buffer of exactly the packet's size. The body is now written where it ends up.
20
+ - A resizable, growable or detached buffer reused through the same `Uint8Array` object read or wrote through a stale view (wrong `overflow`, raw `RangeError` / `TypeError`). The view is rebuilt when the length or offset changes, and an unusable buffer is a `PackError`.
21
+ - `quant(-Number.MAX_VALUE, Number.MAX_VALUE, bits)` encoded everything as 0 and decoded `NaN`: the span overflowed to `Infinity`. Now refused when the schema is built.
22
+ - An array was accepted as a record (`define({ length: u8 }).pack([1, 2, 3, 4])`).
23
+ - `define` made optional fields required in the TypeScript type; they can now be left out.
24
+ - A value too big to allocate a scratch buffer for threw a raw `RangeError`; it is a `PackError` now.
25
+
26
+ `npm run browser` runs 95 checks (spec vectors, ownership, bounds, hostile input, resizable and detached buffers) in a real browser; passed in Chromium 152.
27
+
28
+ `npm run mutate` breaks the code in 35 deliberate ways, one at a time, and checks that the tests notice every one.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1337exp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,150 @@
1
- # Temporary Holding Version
1
+ # 2pack
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **English** · [Русский](README.ru.md)
4
+
5
+ A tiny schema-first binary codec for Node and browsers. You describe a packet once, in TypeScript; the type is inferred, the bytes are small, and decoding hostile input can only ever throw one kind of error. No dependencies, no code generation, you own the buffers.
6
+
7
+ > Version 2 is a rewrite and **not compatible with 1.x**: the field order, the size prefixes and the whole API changed.
8
+
9
+ ## Stack
10
+
11
+ TypeScript 6, ESM only, zero runtime dependencies, Node 18+ and any modern browser. Tests with vitest.
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { array, define, enumOf, i16, quant, str, u8, varuint, type Infer } from '@1337exp/2pack';
17
+
18
+ const Packet = define({
19
+ kind: enumOf(['join', 'move', 'chat']),
20
+ name: str(16), // at most 16 UTF-8 bytes; longer is an error, never truncated
21
+ hp: i16,
22
+ pos: define({ x: quant(-500, 500, 16), y: quant(-500, 500, 16) }), // 2 bytes each, error under 0.008
23
+ items: array(define({ id: varuint, count: u8 }), 8),
24
+ });
25
+
26
+ type Packet = Infer<typeof Packet>; // { kind: 'join' | 'move' | 'chat'; name: string; hp: number; ... }
27
+
28
+ const bytes = Packet.pack({ kind: 'move', name: 'D', hp: -13, pos: { x: 12.5, y: -3 }, items: [{ id: 7, count: 2 }] });
29
+ const value = Packet.unpack(bytes); // typed, validated, or a PackError
30
+ ```
31
+
32
+ Several packets in one buffer (a header, then a payload), with no allocation:
33
+
34
+ ```ts
35
+ const Header = define({ type: u8, version: u8 });
36
+ const Hello = define({ name: str(16), hp: i16 });
37
+ const buf = new Uint8Array(Header.max + Hello.max);
38
+
39
+ let n = Header.packInto(buf, { type: 3, version: 1 });
40
+ n += Hello.packInto(buf, { name: 'D', hp: 100 }, n);
41
+
42
+ const [header, at] = Header.unpackAt(buf, 0, n);
43
+ const payload = Hello.unpack(buf, at, n);
44
+ ```
45
+
46
+ ## Types
47
+
48
+ | | |
49
+ |---|---|
50
+ | `bool` `u8` `i8` `u16` `i16` `u32` `i32` `f32` `f64` | fixed-size, little-endian |
51
+ | `u64` `i64` | 8 bytes, `bigint` |
52
+ | `varuint` `varint` | 1 to 5 bytes, small numbers cost one byte |
53
+ | `str(maxBytes)` `bytes(maxBytes)` | length-prefixed, the maximum is mandatory |
54
+ | `array(item, maxCount)` `optional(item)` `enumOf([...])` | |
55
+ | `quant(min, max, bits)` | a number in a known range stored in 8, 16 or 32 bits |
56
+ | `define({...})` | a record, fields in declaration order, names are not sent |
57
+ | `added(item, default)` | a field added in a newer version, last in its record, see [Evolving a packet](#evolving-a-packet) |
58
+
59
+ Every variable-size type has a declared maximum, so every codec has a finite `min` and `max` size: `new Uint8Array(codec.max)` always fits.
60
+
61
+ The byte-level format is in [docs/spec.md](docs/spec.md), with test vectors in `tests/vectors.ts`.
62
+
63
+ ## Evolving a packet
64
+
65
+ Packets are strict, and **strict is the default**: no names or tags on the wire, a reader needs the schema that wrote the packet, and any byte beyond the last field is an error. Flexibility is opt-in, per message, with `{ extensible: true }`. One kind of change is supported, and it costs nothing on the wire or in speed: **appending fields at the end**.
66
+
67
+ ```ts
68
+ // version 1: declared extensible from the start, so it already ignores fields a later version appends
69
+ const Player = define({ id: u32, name: str(16) }, { extensible: true });
70
+
71
+ // version 2: the new field goes last, wrapped in added(); an older packet decodes with the default
72
+ const Player = define({ id: u32, name: str(16), shield: added(u16, 0) }, { extensible: true });
73
+ ```
74
+
75
+ New server, old client and old server, new client both work: a new reader fills what an old packet lacks, an old reader skips what a new packet adds. The rules and what it cannot do (removing or reordering fields, nesting) are in [docs/spec.md](docs/spec.md#evolution). Two things to know: it relies on the transport telling you where the packet ends (a WebSocket message does), and an extensible record no longer reports extra bytes as an error (that is the price of opting in).
76
+
77
+ ## You own the buffer
78
+
79
+ Three ways to pack, and a page explaining why:
80
+
81
+ | | allocation | the bytes |
82
+ |---|---|---|
83
+ | `pack(value)` | one per packet | yours forever (default, always safe) |
84
+ | `packInto(buf, value, offset?)` | none | in **your** buffer |
85
+ | `packShared(value)` | none | a view, overwritten by the next call |
86
+
87
+ Handing a `packShared` view to `socket.write()` is a real bug under backpressure: [Who owns the bytes](docs/buffer-ownership.md) shows it happening and explains it. `npm run backpressure` reproduces it on your machine.
88
+
89
+ ## Safe by construction
90
+
91
+ - Nothing wraps and nothing is truncated: `300` into `u8`, `1.5` into `u16`, `NaN` into `i32` and a string over its limit are errors that say where (`players[3].hp`).
92
+ - Decoding never reads outside `[offset, end)` and checks every length against both its declared maximum and the bytes that remain. A count of a billion items in a ten-byte packet is rejected before anything is allocated.
93
+ - Hostile or corrupt bytes throw `PackError` and nothing else: checked by tests with truncation of every packet at every length and 40 000 random corruptions.
94
+ - Results are plain objects that own their data. `packInto` writes only the packet's bytes.
95
+ - The same checks run in a real browser: `npm run browser` serves a page that runs the spec vectors, the fuzzing and the resizable-buffer cases against the library and turns the tab title into PASS or FAIL (tried in Chromium; no dependencies).
96
+
97
+ ## How much to trust it
98
+
99
+ A serializer sits on the network edge, so it should be tested like it. What this one has been through:
100
+
101
+ - **338 tests**, and the byte format is pinned by **53 hand-computed vectors** from the spec, each checked packing, unpacking, into an exactly sized buffer and with the surroundings untouched.
102
+ - **Seeded, reproducible fuzzing.** Thousands of random packets round-trip; every packet is truncated at every length; 40 000 random single-byte corruptions and 20 000 random buffers are fed to the decoders. The rule being tested: decoding hostile bytes throws `PackError` and nothing else, and never allocates for a length the input cannot back.
103
+ - **Mutation check** (`npm run mutate`): the code is broken in 35 deliberate ways, one at a time (drop a bounds check, flip a byte order, wrap instead of reject, ...), and the tests must notice every one. They do. The first run let 2 of 19 through, which exposed two weak tests, now fixed.
104
+ - **Independent adversarial review** by another AI model (Grok), which read the spec and wrote its own differential fuzzers against the library. It found 6 bugs, one of them serious (`packInto` wrote bytes after the end of the packet) and missed by our own tests. All fixed, each pinned by a regression test (`tests/regressions.test.ts`).
105
+ - **A real browser**: `npm run browser` runs 95 checks in the page (Chromium 152).
106
+
107
+ What it has **not** had: a human security audit, production traffic, Firefox or Safari, or a big-endian machine. Version 2 is a development build, so pin the exact version until it has been out for a while.
108
+
109
+ ## Speed and size
110
+
111
+ One machine (Node 22, Windows), small objects, median of 5 runs. `npm run bench` reproduces it. Compared on the safe `pack`, which copies.
112
+
113
+ | | size | encode | decode |
114
+ |---|---|---|---|
115
+ | **2pack**, 24-byte game record | **24 B** | **108 ns** | **144 ns** |
116
+ | msgpackr | 57 B | 217 ns | 195 ns |
117
+ | cbor-x | 58 B | 205 ns | 334 ns |
118
+ | JSON (as bytes) | 70 B | 442 ns | 763 ns |
119
+ | **2pack**, snapshot of 20 units | **144 B** | **1246 ns** | **1265 ns** |
120
+ | msgpackr | 608 B | 2164 ns | 3607 ns |
121
+ | cbor-x | 634 B | 2554 ns | 3740 ns |
122
+ | JSON (as bytes) | 792 B | 3849 ns | 4767 ns |
123
+
124
+ The size win is the structural one: with a schema on both sides, names and type tags are not sent. If you cannot share a schema, use msgpackr or cbor-x; they are good.
125
+
126
+ ## Not yet
127
+
128
+ - **Removing or reordering fields.** Only appending is supported; anything else goes through a version in a small header codec (see the spec).
129
+ - **A second implementation.** The format is specified byte for byte so another language can implement it, but only the TypeScript one exists.
130
+ - **A CommonJS build.** ESM only; Node 22 can `require()` it.
131
+
132
+ ## Commands
133
+
134
+ ```bash
135
+ npm run typecheck
136
+ npm test
137
+ npm run build
138
+ npm run bench
139
+ npm run mutate
140
+ npm run backpressure
141
+ npm run browser
142
+ ```
143
+
144
+ ## License
145
+
146
+ MIT, see [LICENSE](LICENSE).
147
+
148
+ ## Us
149
+
150
+ Homepage: [666.io](https://666.io). If you want to help, suggest an idea or just talk, come by.
package/README.ru.md ADDED
@@ -0,0 +1,150 @@
1
+ # 2pack
2
+
3
+ [English](README.md) · **Русский**
4
+
5
+ Крошечный бинарный кодек со схемой для Node и браузеров. Пакет описывается один раз на TypeScript: тип выводится сам, байтов мало, а при разборе враждебных данных можно получить только один вид ошибки. Без зависимостей, без кодогенерации, буферами владеете вы.
6
+
7
+ > Версия 2 — переписанная с нуля и **несовместима с 1.x**: порядок полей, префиксы размеров и весь API изменились.
8
+
9
+ ## Стек
10
+
11
+ TypeScript 6, только ESM, ноль зависимостей в рантайме, Node 18+ и любой современный браузер. Тесты на vitest.
12
+
13
+ ## Быстрый старт
14
+
15
+ ```ts
16
+ import { array, define, enumOf, i16, quant, str, u8, varuint, type Infer } from '@1337exp/2pack';
17
+
18
+ const Packet = define({
19
+ kind: enumOf(['join', 'move', 'chat']),
20
+ name: str(16), // не больше 16 байт UTF-8; длиннее — ошибка, без обрезки
21
+ hp: i16,
22
+ pos: define({ x: quant(-500, 500, 16), y: quant(-500, 500, 16) }), // по 2 байта, ошибка меньше 0,008
23
+ items: array(define({ id: varuint, count: u8 }), 8),
24
+ });
25
+
26
+ type Packet = Infer<typeof Packet>; // { kind: 'join' | 'move' | 'chat'; name: string; hp: number; ... }
27
+
28
+ const bytes = Packet.pack({ kind: 'move', name: 'D', hp: -13, pos: { x: 12.5, y: -3 }, items: [{ id: 7, count: 2 }] });
29
+ const value = Packet.unpack(bytes); // с типом и проверкой, либо PackError
30
+ ```
31
+
32
+ Несколько пакетов в одном буфере (заголовок, потом данные), без аллокаций:
33
+
34
+ ```ts
35
+ const Header = define({ type: u8, version: u8 });
36
+ const Hello = define({ name: str(16), hp: i16 });
37
+ const buf = new Uint8Array(Header.max + Hello.max);
38
+
39
+ let n = Header.packInto(buf, { type: 3, version: 1 });
40
+ n += Hello.packInto(buf, { name: 'D', hp: 100 }, n);
41
+
42
+ const [header, at] = Header.unpackAt(buf, 0, n);
43
+ const payload = Hello.unpack(buf, at, n);
44
+ ```
45
+
46
+ ## Типы
47
+
48
+ | | |
49
+ |---|---|
50
+ | `bool` `u8` `i8` `u16` `i16` `u32` `i32` `f32` `f64` | фиксированный размер, little-endian |
51
+ | `u64` `i64` | 8 байт, `bigint` |
52
+ | `varuint` `varint` | от 1 до 5 байт, малые числа занимают один байт |
53
+ | `str(maxBytes)` `bytes(maxBytes)` | с префиксом длины, максимум обязателен |
54
+ | `array(item, maxCount)` `optional(item)` `enumOf([...])` | |
55
+ | `quant(min, max, bits)` | число из известного диапазона в 8, 16 или 32 битах |
56
+ | `define({...})` | запись, поля в порядке объявления, имена не передаются |
57
+ | `added(item, default)` | поле, добавленное в новой версии, последним в записи, см. [Как менять пакет](#как-менять-пакет) |
58
+
59
+ У каждого типа переменного размера объявлен максимум, поэтому у любого кодека конечные `min` и `max`: `new Uint8Array(codec.max)` всегда вмещает значение.
60
+
61
+ Формат на уровне байтов описан в [docs/spec.md](docs/spec.md), эталонные байты в `tests/vectors.ts`.
62
+
63
+ ## Как менять пакет
64
+
65
+ Пакеты строгие, и **строгий режим по умолчанию**: на проводе нет ни имён, ни тегов, читателю нужна схема, которой записан пакет, а любой байт после последнего поля — ошибка. Гибкость включается явно, для каждого сообщения, через `{ extensible: true }`. Поддерживается один вид изменений, и он ничего не стоит ни в байтах, ни в скорости: **дописывание полей в конец**.
66
+
67
+ ```ts
68
+ // версия 1: сразу объявлена расширяемой, поэтому уже игнорирует поля, которые допишет следующая версия
69
+ const Player = define({ id: u32, name: str(16) }, { extensible: true });
70
+
71
+ // версия 2: новое поле последним, в обёртке added(); старый пакет читается со значением по умолчанию
72
+ const Player = define({ id: u32, name: str(16), shield: added(u16, 0) }, { extensible: true });
73
+ ```
74
+
75
+ Работают оба направления: новый сервер со старым клиентом и старый сервер с новым клиентом. Новый читатель подставляет то, чего нет в старом пакете, старый читатель пропускает то, что дописал новый. Правила и то, чего это не умеет (удалять и переставлять поля, вкладывать), описаны в [docs/spec.md](docs/spec.md#evolution). Две вещи, которые надо знать: нужна граница пакета от транспорта (у сообщения WebSocket она есть), и расширяемая запись больше не считает лишние байты ошибкой (это цена включения).
76
+
77
+ ## Буфер ваш
78
+
79
+ Три способа упаковки и страница, которая объясняет почему:
80
+
81
+ | | аллокации | байты |
82
+ |---|---|---|
83
+ | `pack(value)` | одна на пакет | ваши навсегда (по умолчанию, всегда безопасно) |
84
+ | `packInto(buf, value, offset?)` | нет | в **вашем** буфере |
85
+ | `packShared(value)` | нет | окно, следующий вызов его перезапишет |
86
+
87
+ Отдать окно от `packShared` в `socket.write()` — настоящая ошибка при backpressure: [Чьи это байты](docs/buffer-ownership.ru.md) показывает, как она случается, и объясняет. `npm run backpressure` воспроизводит её у вас на машине.
88
+
89
+ ## Безопасно по построению
90
+
91
+ - Ничего не заворачивается и не обрезается: `300` в `u8`, `1.5` в `u16`, `NaN` в `i32` и строка длиннее лимита — ошибки с указанием места (`players[3].hp`).
92
+ - Разбор никогда не читает за пределами `[offset, end)` и сверяет каждую длину и с объявленным максимумом, и с оставшимися байтами. Миллиард элементов в десятибайтовом пакете отклоняется до всякого выделения памяти.
93
+ - Враждебные и битые байты дают `PackError` и ничего другого: проверено тестами с обрезанием каждого пакета на каждую длину и 40 000 случайных порч.
94
+ - Результаты — обычные объекты, которые владеют своими данными. `packInto` пишет только байты пакета.
95
+ - Те же проверки идут в настоящем браузере: `npm run browser` поднимает страницу, которая прогоняет на библиотеке эталонные векторы, фаззинг и случаи с изменяемыми буферами, а заголовок вкладки становится PASS или FAIL (проверено в Chromium; без зависимостей).
96
+
97
+ ## Насколько ей можно доверять
98
+
99
+ Сериализатор стоит на границе с сетью, поэтому и тестировать его надо соответственно. Что прошла эта библиотека:
100
+
101
+ - **338 теста**, а формат на уровне байтов закреплён **53 эталонными векторами** из спецификации, посчитанными вручную: каждый проверен на упаковку, распаковку, запись в буфер точного размера и на то, что вокруг ничего не тронуто.
102
+ - **Воспроизводимый фаззинг** с зерном. Тысячи случайных пакетов проходят круг упаковки и распаковки; каждый пакет обрезается на каждую длину; в декодеры подаётся 40 000 случайных порч одного байта и 20 000 случайных буферов. Проверяемое правило: разбор враждебных байтов бросает `PackError` и ничего другого и не выделяет память под длину, которую вход не может подтвердить.
103
+ - **Проверка мутациями** (`npm run mutate`): код ломается 35 намеренными способами по одному (убрать проверку границ, перевернуть порядок байт, заворачивать вместо отказа и т. д.), и тесты обязаны заметить каждый. Замечают. С первого раза пропустили 2 из 19, это выявило два слабых теста, теперь исправлены.
104
+ - **Независимое состязательное ревью** другой модели (Grok): она прочитала спецификацию и написала собственные дифференциальные фаззеры против библиотеки. Нашла 6 ошибок, одна серьёзная (`packInto` писал байты после конца пакета), наши тесты её пропустили. Все исправлены, на каждую есть регрессионный тест (`tests/regressions.test.ts`).
105
+ - **Настоящий браузер**: `npm run browser` прогоняет 95 проверок на странице (Chromium 152).
106
+
107
+ Чего у неё **не было**: аудита безопасности людьми, боевого трафика, Firefox и Safari, big-endian машины. Версия 2 — сборка для разработки, поэтому фиксируйте точную версию, пока она не отлежится.
108
+
109
+ ## Скорость и размер
110
+
111
+ Одна машина (Node 22, Windows), мелкие объекты, медиана из 5 прогонов. Воспроизводится `npm run bench`. Сравнение по безопасному `pack`, который копирует.
112
+
113
+ | | размер | кодирование | разбор |
114
+ |---|---|---|---|
115
+ | **2pack**, запись 24 байта | **24 Б** | **108 нс** | **144 нс** |
116
+ | msgpackr | 57 Б | 217 нс | 195 нс |
117
+ | cbor-x | 58 Б | 205 нс | 334 нс |
118
+ | JSON (в байтах) | 70 Б | 442 нс | 763 нс |
119
+ | **2pack**, снимок из 20 юнитов | **144 Б** | **1246 нс** | **1265 нс** |
120
+ | msgpackr | 608 Б | 2164 нс | 3607 нс |
121
+ | cbor-x | 634 Б | 2554 нс | 3740 нс |
122
+ | JSON (в байтах) | 792 Б | 3849 нс | 4767 нс |
123
+
124
+ Выигрыш в размере структурный: когда схема есть у обеих сторон, имена и теги типов не передаются. Если схемой поделиться нельзя, берите msgpackr или cbor-x: они хороши.
125
+
126
+ ## Пока нет
127
+
128
+ - **Удаления и перестановки полей.** Поддерживается только дописывание; остальное идёт через версию в небольшом кодеке-заголовке (см. спецификацию).
129
+ - **Второй реализации.** Формат описан по байтам, чтобы на другом языке можно было реализовать, но существует только TypeScript.
130
+ - **Сборки CommonJS.** Только ESM; Node 22 умеет `require()` для него.
131
+
132
+ ## Команды
133
+
134
+ ```bash
135
+ npm run typecheck
136
+ npm test
137
+ npm run build
138
+ npm run bench
139
+ npm run mutate
140
+ npm run backpressure
141
+ npm run browser
142
+ ```
143
+
144
+ ## Лицензия
145
+
146
+ MIT, см. [LICENSE](LICENSE).
147
+
148
+ ## Мы
149
+
150
+ Домашняя страница: [666.io](https://666.io). Хочешь помочь, предложить идею или просто поговорить — заходи.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * A codec for values of type `T`: one object that knows how to write `T` to bytes and read it back.
3
+ * Every codec has a finite `min` and `max` size in bytes, so `new Uint8Array(codec.max)` always fits any value.
4
+ *
5
+ * Кодек значений типа `T`: умеет писать `T` в байты и читать обратно. У любого кодека конечные `min` и `max`
6
+ * (в байтах), поэтому `new Uint8Array(codec.max)` вмещает любое значение.
7
+ */
8
+ export interface Codec<T> {
9
+ /** Smallest possible encoded size in bytes. / Минимальный размер в байтах. */
10
+ readonly min: number;
11
+ /** Largest possible encoded size in bytes. / Максимальный размер в байтах. */
12
+ readonly max: number;
13
+ /**
14
+ * Encode into a **new** array that you own. Safe to keep, queue or send later. One allocation.
15
+ * Упаковать в **новый** массив, он ваш: его можно хранить, ставить в очередь, отправлять позже.
16
+ */
17
+ pack(data: T): Uint8Array;
18
+ /**
19
+ * Encode into **your** buffer starting at `offset`; returns the number of bytes written.
20
+ * No allocation, and nothing else touches your bytes. Throws `overflow` if the buffer is too small.
21
+ * Упаковать в **ваш** буфер с `offset`; возвращает число записанных байт. Без аллокаций, чужого
22
+ * кода в ваших байтах нет. Если буфера мало, `overflow`.
23
+ */
24
+ packInto(buf: Uint8Array, data: T, offset?: number): number;
25
+ /**
26
+ * Encode into this codec's internal scratch buffer and return a **view** of it. Zero data allocation, but the
27
+ * bytes are overwritten by the next `pack` or `packShared` on this codec: copy them or finish sending
28
+ * before then. Do NOT hand the view to `socket.write()` / `ws.send()` and encode again in the same tick:
29
+ * see docs/buffer-ownership.md.
30
+ * Упаковать во внутренний буфер кодека и вернуть **окно** на него. Данные не копируются, но следующий
31
+ * `pack*` этого кодека перезапишет их. Нельзя отдавать окно в `socket.write()` / `ws.send()` и упаковывать
32
+ * дальше в том же такте: см. docs/buffer-ownership.md.
33
+ */
34
+ packShared(data: T): Uint8Array;
35
+ /**
36
+ * Decode one value from `buf[offset, end)`. The value must fill that range exactly, otherwise `invalid`
37
+ * (trailing bytes) or `truncated`. Throws only `PackError`.
38
+ * Прочитать одно значение из `buf[offset, end)`. Оно должно занять диапазон ровно, иначе `invalid`
39
+ * (лишние байты) или `truncated`. Бросает только `PackError`.
40
+ */
41
+ unpack(buf: Uint8Array | ArrayBuffer, offset?: number, end?: number): T;
42
+ /**
43
+ * Like `unpack`, but extra bytes after the value are allowed; returns the value and the offset after it.
44
+ * Use it to read several values from one buffer.
45
+ * Как `unpack`, но лишние байты после значения допустимы; возвращает значение и смещение за ним.
46
+ */
47
+ unpackAt(buf: Uint8Array | ArrayBuffer, offset?: number, end?: number): [value: T, end: number];
48
+ }
49
+ /** The TypeScript type a codec encodes. / TypeScript-тип, который кодирует кодек. */
50
+ export type Infer<C> = C extends Codec<infer T> ? T : never;
51
+ /** Field names whose value may be `undefined` (they come from `optional(...)`). */
52
+ type OptionalKeys<S extends Record<string, Codec<any>>> = {
53
+ [K in keyof S]: undefined extends Infer<S[K]> ? K : never;
54
+ }[keyof S];
55
+ type Simplify<T> = {
56
+ [K in keyof T]: T[K];
57
+ } & {};
58
+ /**
59
+ * The value type of `define(shape)`: fields from `optional(...)` may be left out.
60
+ * Тип значения для `define(shape)`: поля из `optional(...)` можно не указывать.
61
+ */
62
+ export type Value<S extends Record<string, Codec<any>>> = Simplify<{
63
+ [K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]>;
64
+ } & {
65
+ [K in OptionalKeys<S>]?: Infer<S[K]>;
66
+ }>;
67
+ /** @internal Cursor over a buffer being written. */
68
+ export declare class Writer {
69
+ u8: Uint8Array;
70
+ dv: DataView;
71
+ pos: number;
72
+ /** What `dv` was built for; see `bind`. */
73
+ boundLen: number;
74
+ boundOff: number;
75
+ }
76
+ /** @internal Cursor over a buffer being read; `end` is exclusive. */
77
+ export declare class Reader {
78
+ u8: Uint8Array;
79
+ dv: DataView;
80
+ pos: number;
81
+ end: number;
82
+ boundLen: number;
83
+ boundOff: number;
84
+ }
85
+ /** @internal A codec plus the two functions that composite codecs call. */
86
+ export interface Impl<T> extends Codec<T> {
87
+ write(w: Writer, v: T): void;
88
+ read(r: Reader): T;
89
+ /**
90
+ * The record depends on where its range ends (it has `added` fields or is `extensible`), so it must be the
91
+ * top-level codec: something placed after it could never be found.
92
+ */
93
+ readonly open: boolean;
94
+ /** Set by `added(...)`: the field may be missing from older packets and then takes `fallback`. */
95
+ readonly isAdded?: boolean;
96
+ readonly fallback?: unknown;
97
+ }
98
+ /** @internal */
99
+ export declare function make<T>(min: number, max: number, write: (w: Writer, v: T) => void, read: (r: Reader) => T, open?: boolean, lenient?: boolean): Impl<T>;
100
+ export {};
package/dist/codec.js ADDED
@@ -0,0 +1,168 @@
1
+ import { fail, PackError } from './errors.js';
2
+ // ---------------------------------------------------------------------------------------------------------------
3
+ // Internals / Внутреннее устройство
4
+ // ---------------------------------------------------------------------------------------------------------------
5
+ /** @internal Cursor over a buffer being written. */
6
+ export class Writer {
7
+ u8 = new Uint8Array(0);
8
+ dv = new DataView(this.u8.buffer);
9
+ pos = 0;
10
+ /** What `dv` was built for; see `bind`. */
11
+ boundLen = -1;
12
+ boundOff = -1;
13
+ }
14
+ /** @internal Cursor over a buffer being read; `end` is exclusive. */
15
+ export class Reader {
16
+ u8 = new Uint8Array(0);
17
+ dv = new DataView(this.u8.buffer);
18
+ pos = 0;
19
+ end = 0;
20
+ boundLen = -1;
21
+ boundOff = -1;
22
+ }
23
+ /**
24
+ * Point a cursor at `buf`. The DataView is rebuilt unless it was built for this very array with the same length
25
+ * and offset: a resizable or growable buffer changes length under the same Uint8Array object, and a transferred
26
+ * (detached) one must not be used through a stale view. A zero-length array is always rebuilt, because a
27
+ * detached buffer also reports length 0.
28
+ * Привязать курсор к `buf`. DataView пересоздаётся, если это не тот же массив с той же длиной и смещением
29
+ * (изменяемый буфер меняет длину, отвязанный нельзя использовать через старое окно).
30
+ */
31
+ function bind(c, buf) {
32
+ const len = buf.byteLength;
33
+ const off = buf.byteOffset;
34
+ if (c.u8 === buf && c.boundLen === len && c.boundOff === off && len !== 0)
35
+ return;
36
+ let dv;
37
+ try {
38
+ dv = new DataView(buf.buffer, off, len);
39
+ }
40
+ catch {
41
+ return fail('usage', 'the buffer is detached or cannot be used (was it transferred or resized?)');
42
+ }
43
+ c.u8 = buf;
44
+ c.dv = dv;
45
+ c.boundLen = len;
46
+ c.boundOff = off;
47
+ }
48
+ // One cursor per direction, reused by every call. JS runs one thread of user code at a time and neither
49
+ // direction calls back into user code except through property getters on the data being packed, which the
50
+ // `writing` flag guards. Rebinding the DataView only when the buffer changes keeps repeated calls on the
51
+ // same buffer allocation-free.
52
+ // Один курсор на направление, общий для всех вызовов. DataView пересоздаётся, только когда сменился буфер.
53
+ const writer = new Writer();
54
+ const reader = new Reader();
55
+ let writing = false;
56
+ /** Scratch buffers above this size are not kept between calls. / Буферы больше этого размера не хранятся. */
57
+ const SCRATCH_KEEP = 1 << 20;
58
+ /** @internal */
59
+ export function make(min, max, write, read, open = false, lenient = false) {
60
+ let scratch = null;
61
+ let lastLen = 0;
62
+ function packIntoImpl(buf, data, offset) {
63
+ if (!(buf instanceof Uint8Array))
64
+ fail('type', 'packInto: buf must be a Uint8Array');
65
+ if (!Number.isInteger(offset) || offset < 0 || offset > buf.length) {
66
+ fail('usage', `packInto: offset ${offset} is outside the buffer (length ${buf.length})`);
67
+ }
68
+ if (writing)
69
+ fail('usage', 'pack called while another pack is running (from a getter?)');
70
+ const w = writer;
71
+ bind(w, buf);
72
+ w.pos = offset;
73
+ writing = true;
74
+ try {
75
+ write(w, data);
76
+ }
77
+ catch (e) {
78
+ // A DataView store past the end throws RangeError; that is exactly "buffer too small".
79
+ if (e instanceof RangeError && !(e instanceof PackError)) {
80
+ throw new PackError('overflow', `buffer too small: ${buf.length - offset} bytes available after offset ${offset}`);
81
+ }
82
+ throw e;
83
+ }
84
+ finally {
85
+ writing = false;
86
+ }
87
+ return w.pos - offset;
88
+ }
89
+ /** Encode into the scratch buffer (growing it if `max` is large); result length is left in `lastLen`. */
90
+ function encodeScratch(data) {
91
+ let size = scratch ? scratch.length : Math.min(max, SCRATCH_KEEP);
92
+ for (;;) {
93
+ let buf;
94
+ if (scratch && scratch.length >= size) {
95
+ buf = scratch;
96
+ }
97
+ else {
98
+ try {
99
+ buf = new Uint8Array(size);
100
+ }
101
+ catch {
102
+ // Growing x4 can ask for more than the process can allocate for a value that would have fit.
103
+ throw new PackError('range', `this value needs a buffer of up to ${size} bytes, which cannot be allocated`);
104
+ }
105
+ }
106
+ try {
107
+ lastLen = packIntoImpl(buf, data, 0);
108
+ }
109
+ catch (e) {
110
+ if (e instanceof PackError && e.code === 'overflow' && size < max) {
111
+ size = Math.min(max, size * 4);
112
+ continue;
113
+ }
114
+ throw e;
115
+ }
116
+ if (buf.length <= SCRATCH_KEEP)
117
+ scratch = buf;
118
+ return buf;
119
+ }
120
+ }
121
+ function unpackAtImpl(input, offset, end) {
122
+ const buf = input instanceof ArrayBuffer ? new Uint8Array(input) : input;
123
+ if (!(buf instanceof Uint8Array))
124
+ fail('type', 'unpack: buf must be a Uint8Array or an ArrayBuffer');
125
+ const stop = end === undefined ? buf.length : end;
126
+ if (!Number.isInteger(offset) || !Number.isInteger(stop) || offset < 0 || stop < offset || stop > buf.length) {
127
+ fail('usage', `unpack: range [${offset}, ${stop}) is outside the buffer (length ${buf.length})`);
128
+ }
129
+ const r = reader;
130
+ bind(r, buf);
131
+ r.pos = offset;
132
+ r.end = stop;
133
+ return stop;
134
+ }
135
+ const codec = {
136
+ min,
137
+ max,
138
+ open,
139
+ write,
140
+ read,
141
+ pack(data) {
142
+ const buf = encodeScratch(data);
143
+ return buf.slice(0, lastLen);
144
+ },
145
+ packInto(buf, data, offset = 0) {
146
+ return packIntoImpl(buf, data, offset);
147
+ },
148
+ packShared(data) {
149
+ const buf = encodeScratch(data);
150
+ return buf.subarray(0, lastLen);
151
+ },
152
+ unpack(input, offset = 0, end) {
153
+ const stop = unpackAtImpl(input, offset, end);
154
+ const value = read(reader);
155
+ // Strict by default: every byte must belong to the value. Only a record explicitly declared
156
+ // `extensible` accepts bytes after its last known field (fields a newer writer appended).
157
+ if (!lenient && reader.pos !== stop)
158
+ fail('invalid', `${stop - reader.pos} trailing byte(s) after the value`);
159
+ return value;
160
+ },
161
+ unpackAt(input, offset = 0, end) {
162
+ const stop = unpackAtImpl(input, offset, end);
163
+ const value = read(reader);
164
+ return [value, lenient ? stop : reader.pos];
165
+ },
166
+ };
167
+ return codec;
168
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Why a pack or unpack call failed. See docs/spec.md for the full table.
3
+ * Почему упаковка или распаковка не удалась. Полная таблица в docs/spec.md.
4
+ */
5
+ export type PackErrorCode = 'type' | 'range' | 'overflow' | 'truncated' | 'invalid' | 'usage';
6
+ /**
7
+ * The only error type 2pack throws for bad data. `code` says what went wrong, `path` where
8
+ * (for example `players[3].hp`). Decoding hostile bytes throws nothing else.
9
+ * Единственный тип ошибки 2pack для плохих данных. `code` говорит, что не так, `path` где
10
+ * (например `players[3].hp`). Разбор враждебных байтов не бросает ничего другого.
11
+ */
12
+ export declare class PackError extends Error {
13
+ #private;
14
+ readonly code: PackErrorCode;
15
+ /** Field names and array indexes from the root down to the failing value. / Путь от корня до значения с ошибкой. */
16
+ readonly path: (string | number)[];
17
+ constructor(code: PackErrorCode, detail: string);
18
+ /** @internal Called while the error travels up through objects and arrays. */
19
+ addPath(segment: string | number): void;
20
+ }
21
+ /** @internal */
22
+ export declare function fail(code: PackErrorCode, detail: string): never;