vimp-engine 0.38.1 → 0.38.3

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -6
  3. package/bin/vimp-sim.js +2 -2
  4. package/core/Cargo.toml +4 -3
  5. package/package.json +14 -5
  6. package/src/client/boot.js +30 -7
  7. package/src/client/components/controller/ClientReports.js +1 -1
  8. package/src/client/components/controller/Games.js +1 -1
  9. package/src/client/components/model/ClientReports.js +1 -1
  10. package/src/client/components/model/Games.js +24 -0
  11. package/src/client/components/view/Auth.js +4 -2
  12. package/src/client/components/view/ClientReports.js +1 -1
  13. package/src/client/components/view/Games.js +19 -1
  14. package/src/client/components/view/Lobby.js +1 -7
  15. package/src/client/debug.js +1 -1
  16. package/src/client/lib/authApi.js +1 -1
  17. package/src/client/lib/diagnostics.js +19 -4
  18. package/src/client/lib/engineVersion.js +0 -6
  19. package/src/client/lib/footerLink.js +15 -14
  20. package/src/client/lib/formBuilder.js +9 -8
  21. package/src/client/lib/retryOnNetworkError.js +60 -0
  22. package/src/client/main.js +46 -28
  23. package/src/client/network/HiddenHostHealthLog.js +40 -3
  24. package/src/client/network/HostController.js +4 -4
  25. package/src/client/network/HostHealthPolicy.js +41 -0
  26. package/src/client/network/InlineHostBridge.js +8 -1
  27. package/src/client/network/WebSocketTransport.js +1 -1
  28. package/src/client/session/HandoffFlow.js +5 -0
  29. package/src/client/session/HostRole.js +5 -2
  30. package/src/client/style.css +5 -6
  31. package/src/client/views/gameShell.js +3 -3
  32. package/src/client/views/includes/auth.pug +4 -4
  33. package/src/client/views/includes/lobby.pug +6 -5
  34. package/src/client/views/includes/reports.pug +1 -1
  35. package/src/config/authClient.js +4 -5
  36. package/src/config/gameCodes.js +2 -2
  37. package/src/config/lobby.js +4 -5
  38. package/src/config/opcodes.js +2 -1
  39. package/src/config/wsports.js +2 -2
  40. package/src/devtools/builtinScenario.js +3 -3
  41. package/src/devtools/contract/loadContext.js +2 -2
  42. package/src/devtools/contract/rules/a2-package-scripts.js +4 -3
  43. package/src/devtools/contract/rules/a4-vite-config.js +4 -3
  44. package/src/devtools/contract/rules/a7-package-repository.js +7 -7
  45. package/src/devtools/contract/rules/c4-component-dependencies.js +5 -5
  46. package/src/devtools/contract/rules/index.js +1 -1
  47. package/src/devtools/invariants.js +2 -2
  48. package/src/devtools/pluginLoader.js +3 -3
  49. package/src/host/DebugRecorder.js +1 -1
  50. package/src/host/HostGame.js +35 -15
  51. package/src/host/PortMachine.js +6 -5
  52. package/src/host/commandFailure.js +140 -0
  53. package/src/host/host.worker.js +8 -2
  54. package/src/host/meta/SocketManager.js +2 -2
  55. package/src/host/meta/core/CommandProcessor.js +37 -7
  56. package/src/lib/applyRoomOverrides.js +1 -1
  57. package/src/lib/capabilities.js +1 -1
  58. package/src/lib/checkpointCodec.js +1 -2
  59. package/src/lib/coreConfig.js +1 -1
  60. package/src/lib/createHostRuntime.js +1 -1
  61. package/src/lib/gameId.js +1 -1
  62. package/src/lib/gamePlugin.js +6 -2
  63. package/src/lib/jwt.js +1 -1
  64. package/src/lib/loadGamePackage.js +1 -1
  65. package/src/lib/packageLink.js +21 -63
  66. package/src/lib/validators.js +3 -3
  67. package/src/standalone/index.js +6 -6
  68. package/tests/fixtures/generations/gen-api3/README.md +2 -2
  69. package/tests/fixtures/generations/gen-api4/README.md +2 -2
  70. package/bin/vimp-surface.js +0 -88
  71. package/src/config/env.js +0 -104
  72. package/src/config/master.js +0 -288
  73. package/src/devtools/surface/abiParse.js +0 -175
  74. package/src/devtools/surface/collect.js +0 -413
  75. package/src/lib/clientIp.js +0 -96
  76. package/src/lib/rateLimiter.js +0 -40
  77. package/src/lib/security.js +0 -45
@@ -1,288 +0,0 @@
1
- import path from 'path';
2
- import { fileURLToPath } from 'url';
3
-
4
- // корень репозитория — якорь от расположения файла, не от cwd
5
- const rootDir = path.resolve(
6
- fileURLToPath(import.meta.url),
7
- '..',
8
- '..',
9
- '..',
10
- '..',
11
- '..',
12
- );
13
-
14
- export default {
15
- name: 'VIMP Master Server',
16
- protocol: 'https:',
17
- domain: 'localhost',
18
- // 3000 — игровой сервер, 3001 — Vite HMR (vite.config.js)
19
- port: 3002,
20
-
21
- // сертификаты для локальной разработки — в .certs корня репозитория
22
- // (в продакшене обычный HTTP за Nginx)
23
- httpsOptions: {
24
- key: path.join(rootDir, '.certs', 'key.pem'),
25
- cert: path.join(rootDir, '.certs', 'cert.pem'),
26
- },
27
-
28
- // Статический каталог игр-плагинов. ПУСТ по умолчанию
29
- // (master-game-registry, этап 3): штатный источник каталога — реестр игр
30
- // auth-сервиса, откуда мастер скачивает одобренные пакеты сам
31
- // (master/GameSync.js). Движок игр не знает и зависимостями их не тянет.
32
- //
33
- // Массив остаётся двум потребителям, и env-переопределения у него нет:
34
- // локальной разработке (каталог наполняют собранные пакеты @vimp-games/*
35
- // из node_modules, master/localGames.js — прилинкованная игра важнее
36
- // реестра) и dedicated-серверу, который берёт отсюда игру, названную
37
- // VIMP_DEDICATED_GAME. `package` — имя npm-пакета игры, резолвится через
38
- // node_modules; версию задаёт сама установленная зависимость.
39
- // maxGameScore (snakes-v3 этап 3.3, необязательное поле рядом с
40
- // id/package) — потолок результата ОДНОЙ игры для этой игры; мастер
41
- // клампит им `best`/`points` PUT /auth/rank. Для игр реестра его задаёт
42
- // админ при модерации, здесь — только запасной путь. Не задан — дефолт
43
- // master:playerData:maxGameScore
44
- games: [],
45
-
46
- // Хранилище игровых пакетов (направление master-game-registry): мастер
47
- // качает одобренные игры из npm registry и раздаёт их с диска, вместо того
48
- // чтобы получать их npm-зависимостью на этапе сборки образа.
49
- gameStore: {
50
- // корень хранилища; null → <repoRoot>/.games (локальная разработка).
51
- // В проде задаётся VIMP_GAMES_DIR и монтируется томом
52
- dir: null,
53
- registryUrl: 'https://registry.npmjs.org',
54
- // период опроса реестра auth за изменениями каталога
55
- refreshInterval: 60000,
56
- // потолки распаковки недоверенного архива
57
- maxTarballBytes: 64 * 1024 * 1024,
58
- maxFiles: 5000,
59
- // сколько версий одной игры держать на диске: активная + стейджевая
60
- keepVersions: 2,
61
- // потолок ожидания ответа реестра
62
- timeout: 30000,
63
- },
64
-
65
- // список серверов (GET /servers)
66
- servers: {
67
- // если всего комнат <= порога — региональный фильтр
68
- // и пагинация отключаются, отдаётся весь список
69
- regionThreshold: 15,
70
- defaultLimit: 10, // размер страницы по умолчанию
71
- maxLimit: 50, // максимальный размер страницы
72
- },
73
-
74
- // ограничения регистрируемых комнат
75
- host: {
76
- // санитарная рамка вместимости комнаты для случая, когда игра комнаты
77
- // неизвестна мастеру (gameId: null у старых хостов или id не из
78
- // каталога). Потолок известной игры задаёт её манифест
79
- // (roomDefaults.maxPlayers) — движок его не ограничивает
80
- maxPlayersLimit: 8,
81
- heartbeatTimeout: 30000, // нет heartbeat дольше — хост потерян
82
- sweepInterval: 10000, // период уборки реестра комнат
83
- },
84
-
85
- // жизнь комнаты (host-migration, этап 2): комната живёт, пока в ней есть
86
- // участник
87
- room: {
88
- // отсоединённый участник ещё считается в комнате (реконнект сигналинга)
89
- memberGraceMs: 15000,
90
- // обрыв WS хоста запускает миграцию не сразу, а через столько, если хост
91
- // не вернулся reclaim_host'ом и ни один гость не сообщил host_unreachable
92
- // (host_leaving мигрирует сразу); reclaim_host после начала миграции, до
93
- // регистрации преемника, её отменяет (этап 7.0)
94
- hostDisconnectGraceMs: 2000,
95
- // повышать некого — комната ждёт reclaim_host столько, потом уборка
96
- // мигрирует или закрывает её
97
- hostReclaimGraceMs: 10000,
98
- // потолок строки карточки комнаты в лобби (gameConfig.lobbyInfo игры)
99
- maxInfoLength: 48,
100
- // RTT участников (host-migration этап 6): ws.ping() каждой сессии с
101
- // этим периодом; EMA RTT и джиттера — единая линейка связности для
102
- // хоста и кандидатов
103
- rttProbeIntervalMs: 5000,
104
- // сессия без pong дольше — terminate(): «тихо умерший» хост находится
105
- // быстрее, чем по heartbeatTimeout
106
- wsDeadAfterMs: 12000,
107
- // преемник (бета): кандидат должен пробыть в комнате не меньше этого
108
- minMemberAgeMs: 10000,
109
- // плановый пересчёт преемника (помимо join/leave/member_update)
110
- successorReviewMs: 15000,
111
- // гистерезис смены беты: лучший кандидат держит score не хуже
112
- // successorSwitchRatio × score текущего дольше successorSwitchSustainMs
113
- successorSwitchSustainMs: 30000,
114
- successorSwitchRatio: 0.65,
115
- // аварийная миграция (этап 7): бета — преемник с точкой, если она
116
- // получила её не раньше checkpointMaxAgeMs назад (по standby_status.ageMs;
117
- // статус идёт раз в 5 с — окно больше двух периодов), иначе cold-промоушен любого
118
- // способного; преемник не занял комнату за promotionTimeoutMs
119
- // (coldPromotionTimeoutMs — он перезагружает страницу) — следующий
120
- checkpointMaxAgeMs: 12000,
121
- promotionTimeoutMs: 10000,
122
- coldPromotionTimeoutMs: 25000,
123
- // отчёты host_unreachable: хост не ответил на probe за probeTimeoutMs —
124
- // потерян; ответил, но за reportWindowMs отчиталась половина гостей —
125
- // принудительная миграция, не чаще forcedMigrationCooldownMs на комнату.
126
- // Гости и отчёты считаются по аккаунтам; кворум — не меньше
127
- // minUnreachableReporters разных пользователей (в паре с плохим P2P один
128
- // гость не снимает хоста — иначе роль «пинг-понгом» ходит по кругу)
129
- probeTimeoutMs: 2000,
130
- reportWindowMs: 5000,
131
- forcedMigrationCooldownMs: 30000,
132
- minUnreachableReporters: 2,
133
- // плановая передача (этап 8): преемник не занял комнату за
134
- // handoffTimeoutMs — передача отменяется, хост размораживает матч
135
- handoffTimeoutMs: 8000,
136
- // host_migrating.waitMs — дедлайн текущей попытки промоушена (передачи)
137
- // плюс migrationNoticeMarginMs: столько гость ждёт host_changed
138
- migrationNoticeMarginMs: 5000,
139
- // автотриггеры (этап 9c). Сетевой лаг хоста: медиана его RTT до гостей
140
- // (host_health) выше lagRttThresholdMs непрерывно lagSustainMs, а score
141
- // беты лучше score хоста хотя бы на lagImprovementRatio — хосту
142
- // request_handoff. Не раньше autoMigrationCooldownMs с прошлой
143
- // авто-смены хоста в комнате (overload/hidden/network — кулдаун общий)
144
- // и с получения роли текущим хостом
145
- lagRttThresholdMs: 250,
146
- lagSustainMs: 10000,
147
- lagImprovementRatio: 0.35,
148
- autoMigrationCooldownMs: 90000,
149
- // бета — не ниже этого FPS рендера (caps.fps гостя; неизвестный FPS не
150
- // отсеивает). Аварийный промоушен порог не применяет
151
- minSuccessorFps: 30,
152
- // голосование «Change host» (этап 10, /changehost; только лобби-режим):
153
- // голоса считает мастер. Длительность голосования; повторный старт в
154
- // комнате и тем же пользователем — не раньше кулдаунов; прошедшее
155
- // голосование просит хоста отдать роль, не начал за voteForceAfterMs —
156
- // принудительная миграция; снятый хост не бета и не хост комнаты
157
- // demotedCooldownMs (кроме случая, когда больше принять некому).
158
- // Один голос на аккаунт; голосует и начинает участник, пробывший в
159
- // комнате не меньше minVoterAgeMs (свежие вкладки не накручивают голоса).
160
- // После смены хоста голосование ждёт первого room_peers нового хоста —
161
- // до него подтверждёнными считаются все, и фантомы попали бы в число
162
- // голосующих, — но не дольше peersReportGraceMs (больше
163
- // lobby.migration.peersReportIntervalMs: потерянный первый отчёт
164
- // повторится внутри окна; хост старше room_peers отчёта не пришлёт)
165
- vote: {
166
- hostVoteDurationMs: 15000,
167
- roomVoteCooldownMs: 120000,
168
- userStartCooldownMs: 60000,
169
- voteForceAfterMs: 5000,
170
- demotedCooldownMs: 600000,
171
- minVoterAgeMs: 30000,
172
- peersReportGraceMs: 20000,
173
- },
174
- // GET /rooms/:roomId (прямая ссылка, этап 3) на IP: открытие ссылки —
175
- // один запрос, лимит только против перебора roomId
176
- lookupRateLimit: {
177
- limit: 20,
178
- windowMs: 1000,
179
- },
180
- },
181
-
182
- // GET /auth/leaderboard (code review L2): TTL кэша на мастере (мс) и
183
- // верхняя граница ?limit= — публичный анонимный эндпоинт, самый частый
184
- // запрос лобби, выборка меняется медленно
185
- leaderboard: {
186
- cacheTtl: 15000,
187
- maxLimit: 100,
188
- },
189
-
190
- // GET /auth/placement + агрегирующий GET /auth/placements (snakes-v3
191
- // этап 3.3): место меняется медленно, а каждый вход участника стоит трёх
192
- // срезов. Кэш здесь про round-trip до auth; стоимость самого запроса
193
- // снята на стороне auth (RankDistribution)
194
- placement: {
195
- cacheTtl: 30000,
196
- },
197
-
198
- // пределы записи профилей в БД (snakes-v3 этап 3, решение пользователя 9):
199
- // «игр сотни, серверов сотни» — минимальный интервал держит движок на
200
- // стороне хоста, а мастер держит потолок для сломанного или злонамеренного
201
- // сервера, который этот интервал обошёл
202
- playerData: {
203
- // PUT /auth/rank + /auth/state на комнату (проверенный roomId) в минуту.
204
- // Честная комната на 32 при lobbyConfig.playerData.minFlushInterval в
205
- // 5 минут пишет 64 запроса за эти 5 минут, то есть ~13/мин; остальное —
206
- // запас на срочные границы (уход участника обходит интервал), и его
207
- // хватает даже комнате, полностью сменившей состав дважды за минуту.
208
- // Потолок держит не честную комнату, а сломанную или злонамеренную,
209
- // поэтому запас считается от честной, а не «пусть будет побольше»
210
- writesPerMinute: 120,
211
- // потолок результата ОДНОЙ игры, если игра не объявила свой
212
- // (master:games[].maxGameScore): обоснование — plan/snakes-v3/stage_2.md
213
- maxGameScore: 10000,
214
- },
215
-
216
- // заголовки безопасности (гигиена среды, Этап 5.4). CSP на статику/.wasm в
217
- // проде ставит Nginx (см. docs/deployment.md) — здесь single source of truth
218
- // политики; мастер применяет её к своим ответам только в проде (в dev CSP
219
- // сломала бы Vite HMR). WASM требует 'wasm-unsafe-eval', Worker — 'blob:';
220
- // connect-src data: — PixiJS фетчит тестовый data:-URL для проверки ImageBitmap.
221
- // authServiceUrl — домен central auth-сервиса (packages/auth): лобби
222
- // делает туда прямой fetch (браузерные ручки auth-сервиса), поэтому
223
- // connect-src должен его разрешать; сам OAuth-редирект (location.href на
224
- // auth-сервис/провайдера) CSP не ограничивает — это навигация верхнего
225
- // уровня, не fetch/XHR. challenges.cloudflare.com в script-src и frame-src
226
- // — скрипт и iframe виджета Turnstile на экране регистрации.
227
- // script-src несёт sha256-хэш инлайнового importmap из index.html (для
228
- // pixi.js — src="..." на <script type="importmap"> браузеры не
229
- // поддерживают, инлайн обязателен). Хэш посчитан по факту собранного
230
- // packages/engine/dist/index.html скриптом
231
- // scripts/check-importmap-csp-hash.mjs (запускается postbuild) — vite
232
- // build минифицирует HTML и может изменить байты скрипта, поэтому хэш
233
- // нельзя брать из исходника/консоли браузера; при правке importmap или
234
- // апгрейде Vite смотреть на вывод postbuild-проверки.
235
- security: {
236
- authServiceUrl: 'http://localhost:3010',
237
- csp: authServiceUrl =>
238
- [
239
- "default-src 'self'",
240
- "script-src 'self' 'wasm-unsafe-eval' 'sha256-XJmzkFBLHYpcM8KgGRFztTJTwfMb5xIFKAmqlgTpobo=' https://challenges.cloudflare.com",
241
- "worker-src 'self' blob:",
242
- 'frame-src https://challenges.cloudflare.com',
243
- `connect-src 'self' wss: data:${authServiceUrl ? ` ${authServiceUrl}` : ''}`,
244
- "img-src 'self' data: blob:",
245
- "style-src 'self' 'unsafe-inline'",
246
- "object-src 'none'",
247
- "base-uri 'self'",
248
- "frame-ancestors 'none'",
249
- ].join('; '),
250
- referrerPolicy: 'no-referrer',
251
- },
252
-
253
- // журнал клиентских ошибок (plan/client-reports): приём POST
254
- // /client-reports на боксе и пересылка пачками в auth-сервис. Пустой
255
- // token — пересылка выключена, остаётся строка в журнале процесса
256
- clientReports: {
257
- token: '',
258
- flushIntervalMs: 30000,
259
- forwardBatch: 50,
260
- forwardTimeoutMs: 5000,
261
- maxPending: 500, // потолок разных отпечатков в буфере
262
- logSeenMax: 5000, // сколько отпечатков процесс помнит «уже печатал»
263
- // бюджет НОВЫХ отпечатков на бокс (решение 9 плана): распределённый спам
264
- // обходит лимит по IP, а новая строка в auth — главный ресурс. Повторы
265
- // известных отпечатков бюджет не тратят
266
- newFingerprintsPerMinute: 60,
267
- // ключ — адрес, для IPv6 — подсеть /64 (lib/clientIp.js → rateLimitKey)
268
- // клиент шлёт не чаще раза в 10 с — запас на несколько вкладок за одним
269
- // адресом (NAT); главная защита — бюджет новых отпечатков
270
- rateLimit: { limit: 30, windowMs: 60000 },
271
- bodyLimit: '16kb',
272
- maxItemsPerRequest: 10,
273
- },
274
-
275
- // заголовок с регионом хоста от Nginx/CDN (например, CF-IPCountry);
276
- // выбран вместо geoip-lite — бесплатнее по памяти
277
- regionHeader: 'x-region',
278
-
279
- // лимит сигнальных ping-запросов с одного IP (защита от DDOS)
280
- pingRateLimit: {
281
- limit: 10,
282
- windowMs: 1000,
283
- },
284
-
285
- // ICE-конфигурация для установки P2P-соединений:
286
- // STUN обязателен; TURN — опциональный релей по итогам Этапа 0
287
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
288
- };
@@ -1,175 +0,0 @@
1
- // Разбор `core/src/abi.rs` — единственный раздел слепка плагинной
2
- // поверхности, который нельзя собрать импортом: методы wasm-ABI живут внутри
3
- // `macro_rules!`, раскрывающихся уже в крейте игры. Слепок читает файл как
4
- // текст и вытаскивает имена и нормализованные сигнатуры `pub fn`.
5
- //
6
- // Разбор обязан ПАДАТЬ, а не возвращать пустоту: молчаливо пустой раздел
7
- // слепка пропустит любое нарушение И1/И3 (plan/plugin-forward-compat).
8
-
9
- // имя макроса → раздел слепка (`abi.game` / `abi.client`)
10
- const MACRO_SECTIONS = {
11
- // имена макросов Rust — строками: camelCase к ним неприменим
12
- 'export_game_core_abi': 'game',
13
- 'export_client_core_abi': 'client',
14
- };
15
-
16
- /**
17
- * @param {string} source - Содержимое `core/src/abi.rs`.
18
- * @returns {{game: Object[], client: Object[]}} Методы ABI по разделам,
19
- * каждый — { name, args, ret }, отсортированные по имени.
20
- */
21
- export function parseAbi(source) {
22
- const blocks = splitMacros(source);
23
-
24
- if (blocks.length === 0) {
25
- throw new Error(
26
- 'abiParse: no macro_rules! block found in core/src/abi.rs — the file ' +
27
- 'was restructured; fix the parser instead of letting the surface ' +
28
- 'snapshot go silently empty',
29
- );
30
- }
31
-
32
- const sections = {};
33
-
34
- for (const block of blocks) {
35
- const section = MACRO_SECTIONS[block.macro];
36
-
37
- if (!section) {
38
- continue; // макрос вне ABI плагина — не часть поверхности
39
- }
40
-
41
- const methods = parseMethods(block.body);
42
-
43
- if (methods.length === 0) {
44
- throw new Error(
45
- `abiParse: macro_rules! ${block.macro} yielded no "pub fn" — the ` +
46
- 'file was restructured; fix the parser',
47
- );
48
- }
49
-
50
- sections[section] = methods.sort((a, b) => a.name.localeCompare(b.name));
51
- }
52
-
53
- for (const [macro, section] of Object.entries(MACRO_SECTIONS)) {
54
- if (!sections[section]) {
55
- throw new Error(
56
- `abiParse: macro_rules! ${macro} not found in core/src/abi.rs`,
57
- );
58
- }
59
- }
60
-
61
- return sections;
62
- }
63
-
64
- // тело каждого `macro_rules! <name> { … }` — от имени до парной скобке
65
- function splitMacros(source) {
66
- const re = /macro_rules!\s+([a-z_0-9]+)\s*\{/g;
67
- const blocks = [];
68
- let match;
69
-
70
- while ((match = re.exec(source)) !== null) {
71
- const open = re.lastIndex - 1;
72
- const close = matchBrace(source, open, '{', '}');
73
-
74
- blocks.push({
75
- macro: match[1],
76
- body: source.slice(open + 1, close),
77
- });
78
- }
79
-
80
- return blocks;
81
- }
82
-
83
- // `pub fn name(args) -> ret` внутри тела макроса; тело метода пропускается
84
- function parseMethods(body) {
85
- const re = /pub\s+fn\s+([a-z_0-9]+)\s*\(/g;
86
- const methods = [];
87
- let match;
88
-
89
- while ((match = re.exec(body)) !== null) {
90
- const open = re.lastIndex - 1;
91
- const close = matchBrace(body, open, '(', ')');
92
- const tail = body.slice(close + 1);
93
- const arrow = /^\s*->([^{;]+)/.exec(tail);
94
-
95
- methods.push({
96
- name: match[1],
97
- args: splitArgs(body.slice(open + 1, close)).map(normalizeType),
98
- ret: arrow ? normalizeType(arrow[1]) : '()',
99
- });
100
- }
101
-
102
- return methods;
103
- }
104
-
105
- // индекс скобки, парной открывающей на позиции `open`
106
- function matchBrace(source, open, openChar, closeChar) {
107
- let depth = 0;
108
-
109
- for (let i = open; i < source.length; i += 1) {
110
- if (source[i] === openChar) {
111
- depth += 1;
112
- } else if (source[i] === closeChar) {
113
- depth -= 1;
114
-
115
- if (depth === 0) {
116
- return i;
117
- }
118
- }
119
- }
120
-
121
- throw new Error(`abiParse: unbalanced ${openChar} at offset ${open}`);
122
- }
123
-
124
- // список аргументов по запятым верхнего уровня; получатель (`&self`,
125
- // `&mut self`) в сигнатуру не входит — он не часть бинарного контракта
126
- function splitArgs(list) {
127
- const args = [];
128
- let depth = 0;
129
- let current = '';
130
-
131
- for (const ch of list) {
132
- if (ch === '<' || ch === '(' || ch === '[') {
133
- depth += 1;
134
- } else if (ch === '>' || ch === ')' || ch === ']') {
135
- depth -= 1;
136
- }
137
-
138
- if (ch === ',' && depth === 0) {
139
- args.push(current);
140
- current = '';
141
- continue;
142
- }
143
-
144
- current += ch;
145
- }
146
-
147
- args.push(current);
148
-
149
- return args
150
- .map(arg => arg.trim())
151
- .filter(arg => arg.length > 0 && !/^&?\s*(mut\s+)?self$/.test(arg))
152
- .map(arg => {
153
- const colon = arg.indexOf(':');
154
-
155
- // аргумент без имени в этих макросах не встречается: имя есть всегда
156
- return colon === -1 ? arg : arg.slice(colon + 1);
157
- });
158
- }
159
-
160
- // Стабильная строка типа: меняется тогда и только тогда, когда меняется
161
- // бинарный контракт. Ссылка/`mut`/пробелы/путь до wasm-bindgen на него не
162
- // влияют; `Result<T, JsError>` сворачивается в `Result<T>` — вариант ошибки
163
- // у всех методов один.
164
- function normalizeType(type) {
165
- const flat = type
166
- .replaceAll('::wasm_bindgen::', '')
167
- .replaceAll('wasm_bindgen::', '')
168
- .replace(/\bmut\b/g, '')
169
- .replaceAll('&', '')
170
- .replace(/\s+/g, '');
171
-
172
- return flat.replace(/^Result<(.*),JsError>$/, 'Result<$1>');
173
- }
174
-
175
- export default parseAbi;