merge origin/master into codex/wechat-mini-program-virtual-payment

This commit is contained in:
kdletters
2026-05-31 23:00:43 +08:00
278 changed files with 2904 additions and 2129 deletions

View File

@@ -4,6 +4,14 @@ name = "Genarrative"
[setup]
script = '''
cp "$CODEX_SOURCE_TREE_PATH\.env.secrets.local" "$CODEX_WORKTREE_PATH\.env.secrets.local"
npm install
npm run codegraph:init
npm run codegraph:index
'''
[setup.win32]
script = '''
cp "$env:CODEX_SOURCE_TREE_PATH\.env.secrets.local" "$env:CODEX_WORKTREE_PATH\.env.secrets.local"
npm install
npm run codegraph:init

View File

@@ -23,6 +23,13 @@
- 影响范围:`src/services/payment/paymentPlatform.ts``src/components/rpg-entry/RpgEntryHomeView.tsx``miniprogram/pages/wechat-pay/``server-rs/crates/api-server/src/runtime_profile.rs``server-rs/crates/shared-contracts/src/runtime.rs``packages/shared/src/contracts/runtime.ts`、微信登录态存储。
- 验证方式:泥点和会员商品在小程序运行态都请求 `wechat_mp_virtual`;小程序页能按 payload 调用 `wx.requestVirtualPayment` / `wx.requestPayment``cargo check -p api-server --manifest-path server-rs/Cargo.toml` 与支付相关前端测试通过。
- 关联文档:`docs/【技术方案】微信虚拟支付接入-2026-05-26.md`
## 2026-05-30 Linux 本地 dev 端口段按系统级注册表分配
- 背景:同一台 Linux 开发机上有多个用户同时跑 `npm run dev` 时,单纯靠各自 `GENARRATIVE_DEV_PORT_RANGE` 容易撞段,且同一用户并发起两个 dev 会话时也会把相同端口段重复拿走。
- 决策Linux 上的本地 dev 端口段分配统一收口到系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json`,锁文件为 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。未手动指定时自动从 `10000-10099` 开始按 100 端口块分配,后续块按 `10100-10199``10200-10299` 递增;端口段映射固定为 `web = start``api = start + 1``spacetime = start + 2``admin-web = start + 3`;注册表会拒绝不同用户的相同或重叠段,并让同一用户后续启动继续复用自己已占用的固定段。`GENARRATIVE_DEV_PORT_RANGE``--port-range` 仍可手动指定端口段,但只在 Linux 生效Windows 继续沿用原有端口探测与漂移逻辑,不读注册表。
- 影响范围:`scripts/dev-stack-port-utils.mjs``scripts/dev.mjs``scripts/dev-stack-port-utils.test.ts``scripts/dev.test.ts``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、本条决策记录、`development-workflow.md`
- 验证方式:`node --check scripts/dev-stack-port-utils.mjs``node --check scripts/dev.mjs``node node_modules/vitest/vitest.mjs run scripts/dev-stack-port-utils.test.ts scripts/dev.test.ts` 通过Linux 下能看到 `[dev] port-range:``registry.json` 路径日志,自动分配从 `10000-10099` 起步Windows 不出现注册表分配日志。
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-05-27 生成页总进度圆弧锁定固定画布

View File

@@ -50,6 +50,8 @@ npm install
npm run dev
```
Linux 多用户共享同一台机器开发时,本地 dev 脚本会为当前 Linux 用户分配一个固定端口段并写入系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json`,自动分配从 `10000-10099` 开始,每段 100 个端口,四个 dev 服务依次使用 `start``start + 3`。可用 `GENARRATIVE_DEV_PORT_RANGE``npm run dev -- --port-range` 手动指定端口段用于特殊场景;注册表会阻止不同用户使用相同或重叠段,并让同一用户后续启动继续复用自己已占用的固定段。该机制只在 Linux 生效Windows 仍沿用原有端口探测与漂移逻辑。
该命令会启动:
- SpacetimeDB standalone

View File

@@ -151,6 +151,14 @@
- 验证:检查 `jenkins/Jenkinsfile.production-stdb-module-publish` 文件开头字节不再是 `EF BB BF`,并用 Jenkins `validateDeclarativePipeline` 或重放 `Genarrative-Stdb-Module-Publish`,不应再停在 `No such DSL method 'pipeline'`
- 关联:`jenkins/Jenkinsfile.production-stdb-module-publish``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## Linux 多用户 dev 端口冲突先查系统级端口段注册表
- 现象:同一台 Linux 机器上多个用户同时开发时,`npm run dev` 报端口段已被其他用户占用、同一用户已有活跃端口段,或 SpacetimeDB 复用记录指向当前用户端口段之外的地址;未手动指定时自动分配应从 `10000-10099` 起步。
- 原因Linux dev 脚本会通过 `/var/tmp/genarrative-dev-port-ranges/registry.json` 做系统级端口段分配,避免两个用户配置相同或重叠端口段;同一用户后续启动会继续复用自己已经占用的固定端口段。注册表会保留该用户的段记录,不会因为多开而要求重新分配。
- 处理:先确认当前用户已经占用的端口段,再让后续 `npm run dev` / `dev:*` 继续沿用这段;如确实要切换段,手动释放或清掉对应 registry 记录后再重启。需要临时隔离测试时用 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR=<tmp-dir>` 覆盖注册表目录。不要在 Windows 上按这个注册表排查Windows 仍走原有端口探测与漂移逻辑。未指定端口段时,系统会从 `10000-10099` 开始顺序分配。
- 验证:重新启动后终端应打印 `[dev] port-range: <start-end> (<user>)``[dev] port-range-registry: .../registry.json``node node_modules/vitest/vitest.mjs run scripts/dev-stack-port-utils.test.ts scripts/dev.test.ts` 应通过 Linux registry、自动分配 `10000-10099` 与 Windows bypass 用例。
- 关联:`scripts/dev-stack-port-utils.mjs``scripts/dev.mjs``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## SpacetimeDB 入口迁移 helper 合并时不要只保留调用
- 现象:`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 或 Jenkins `Genarrative-Stdb-Module-Build``E0425 cannot find function migrate_rpg_entry_from_old_hidden_default in this scope`,位置在 `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs` 的默认入口配置播种流程。

View File

@@ -1,6 +1,6 @@
# Genarrative 项目共享概览
更新时间:`2026-05-15`
更新时间:`2026-05-29`
## 一句话定位
@@ -33,6 +33,8 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A
server-rs + Axum + SpacetimeDB
```
当前 SpacetimeDB crate、SDK、CLI / standalone、生成 bindings 和容器压测镜像统一按 `2.3.0` 对齐。
职责边界:
- `api-server`HTTP / SSE / BFF 门面和外部副作用编排。

View File

@@ -34,6 +34,8 @@ metadata:
端口不可用时,脚本会从优先端口开始向后寻找可用端口。后续流程必须以解析后的实际端口为准,不能继续使用默认端口。
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE``--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start``api = start + 1``spacetime = start + 2``adminWeb = start + 3`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199``10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效Windows 继续沿用原有端口探测、漂移和复用逻辑,不读系统级注册表。
## 实现入口
- `package.json`
@@ -43,10 +45,12 @@ metadata:
- `isPortAvailable(...)`:探测端口是否可监听。
- `findAvailablePort(...)`:从优先端口向后寻找可用端口,`0` 表示申请临时端口。
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,并避免本次解析结果互相冲突。
- Linux 注册表分配:`reserveLinuxDevPortRange(...)` / `releaseLinuxDevPortRange(...)`,仅在 Linux 上启用系统级端口段登记与用户段复用,自动分配从 `10000-10099` 起。
- CLI 模式:`node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:3101 api:127.0.0.1:8082 web:0.0.0.0:3000 adminWeb:127.0.0.1:3102`
- `scripts/dev.mjs`
- 解析 CLI 参数后统一计算 client host、端口、`SPACETIME_SERVER``RUST_SERVER_TARGET`
- 完整栈按 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 顺序启动。
- Linux 下会先申请系统级端口段并把它映射成四个 dev 端口;自动分配从 `10000-10099`Windows 则直接沿用原有参数解析与端口漂移逻辑。
- 单模块命令复用同一套参数和 env 解析。
## 必须保持的传递链路
@@ -60,6 +64,7 @@ metadata:
5. 主站 Vite`RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET``ADMIN_WEB_TARGET``ADMIN_WEB_PORT``--port=${WEB_PORT}``--host=${WEB_HOST}`
6. 后台 Vite`ADMIN_API_TARGET``GENARRATIVE_API_TARGET``GENARRATIVE_API_PORT``--port=${ADMIN_WEB_PORT}`
7. 控制台日志:`[dev:ports]``[dev] web/admin web/api-server/spacetime` 必须显示最终实际地址。
8. Linux 端口段注册:`[dev] port-range:``[dev] port-range-registry:` 只在 Linux 输出Windows 不应依赖系统级注册表。
如果只改了其中一段,通常会出现:浏览器打开的前端可用,但 `/api/*` 代理到旧端口;后台页面可用但后台 API 失败SpacetimeDB 启动在新端口但 publish 仍发往旧端口。
@@ -76,6 +81,7 @@ metadata:
4. 修改 watch 时保持模块边界SpacetimeDB 只监听 `spacetime-module` 且改动后重新 publish不重启 standalone 宿主api-server 排除 `spacetime-module`web/admin-web 源码变化交给 Vite 自身 HMR外层调度器不要再监听前端目录重启 Vite。
5. 修改 `dev:web` 时不要自动改后端目标策略;`dev:web` 只负责主站 Vite 端口可用性与已有后端目标选择。
6. 同步更新技术文档和团队共享记忆。
7. 如果修改 Linux 端口段注册口径,确认 Windows 分支仍保持旧行为,不要把系统级注册表逻辑扩散到 Windows。
## 测试与验证
@@ -113,6 +119,7 @@ node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 ap
## 验收清单
- [ ] 端口工具有测试覆盖端口被占用和多端口互斥解析。
- [ ] Linux 注册表分配、同用户复用固定段并继续漂移、自动分配从 `10000-10099` 起、Windows bypass 都有测试覆盖。
- [ ] `scripts/dev.mjs` 通过 `node --check`
- [ ] `npm run dev` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 都使用实际端口。
- [ ] `npm run dev:web` 在主站端口不可用时能切换到可用端口。

View File

@@ -16,6 +16,14 @@ export default defineConfig(({mode}) => {
env.GENARRATIVE_API_TARGET ||
`http://127.0.0.1:${env.GENARRATIVE_API_PORT || '3100'}`;
const base = env.ADMIN_WEB_BASE || '/admin/';
const ignoredWatchGlobs = [
'**/.git/**',
'**/.worktrees/**',
'**/dist/**',
'**/node_modules/**',
'**/server-rs/**',
'**/server-rs/target/**',
];
return {
root: adminWebRoot,
@@ -23,6 +31,9 @@ export default defineConfig(({mode}) => {
base,
plugins: [react()],
server: {
watch: {
ignored: ignoredWatchGlobs,
},
proxy: {
'/admin/api': {
target: apiTarget,

View File

@@ -55,7 +55,7 @@ Linux Docker Engine 若要从宿主机 CLI 连到容器内服务,直接用 `ht
## 构建工具链
`api-server` 容器镜像只构建 Linux release API 二进制,不构建 `spacetime-module`。当前 `api-server -> spacetime-client -> spacetimedb-sdk 2.2.0` 依赖链要求 Rust 1.93,因此 `deploy/container/api-server.Dockerfile` 的 Rust builder 固定为 `rust:1.93-bookworm`。如果本机 Docker Hub 拉取失败,可以先在本机准备同名本地 builder 镜像,但不要把临时 bootstrap 容器或私有 registry 凭据写入仓库。
`api-server` 容器镜像只构建 Linux release API 二进制,不构建 `spacetime-module`。当前 `api-server -> spacetime-client -> spacetimedb-sdk 2.3.0` 依赖链要求 Rust 1.93,因此 `deploy/container/api-server.Dockerfile` 的 Rust builder 固定为 `rust:1.93-bookworm`。如果本机 Docker Hub 拉取失败,可以先在本机准备同名本地 builder 镜像,但不要把临时 bootstrap 容器或私有 registry 凭据写入仓库。
## 启动与验证

View File

@@ -2,7 +2,7 @@ name: genarrative-container-loadtest
services:
spacetimedb:
image: clockworklabs/spacetime:v2.2.0
image: clockworklabs/spacetime:v2.3.0
user: root
command:
[

View File

@@ -16,6 +16,8 @@ server-rs + Axum + SpacetimeDB
`server-rs/Cargo.toml` 是 workspace 事实源。默认构建成员为 `crates/api-server`;第三方依赖版本和 workspace 内 crate path 统一放在 `[workspace.dependencies]`
SpacetimeDB 版本口径:当前 Rust crate `spacetimedb``spacetimedb-sdk``spacetimedb-lib` 统一锁定 `2.3.0`;本地 `spacetime` CLI / standalone、生成的 `spacetime-client` bindings 和容器压测镜像也必须与 `2.3.0` 对齐,避免 BSATN / procedure result 反序列化错配。
当前主要 crate
- HTTP 服务:`api-server`

View File

@@ -43,6 +43,8 @@ npm run dev:web
npm run dev:api-server
```
Linux 本机多用户并发开发时,`npm run dev``npm run dev:*` 单模块命令会先在系统级端口段注册表里给当前用户分配一个端口段,再把该段映射为 `web = start``api = start + 1``spacetime = start + 2``admin-web = start + 3`。默认注册表目录是 `/var/tmp/genarrative-dev-port-ranges/`,其中 `registry.json` 记录各用户的活跃段,`registry.lock` 负责串行化分配;可以用 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。系统自动分配时从 `10000-10099` 开始,每次占用 100 个端口块,后续块按 `10100-10199``10200-10299` 递增;`GENARRATIVE_DEV_PORT_RANGE``--port-range` 只在 Linux 上生效Windows 仍按原来的 3000 / 8082 / 3101 / 3102 端口探测与漂移逻辑运行,不读这个系统级注册表。
后端日志默认写入 `logs/api-server/`。后端 API smoke 使用 `npm run dev:api-server` 并检查 `/healthz`;不要使用旧 `api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径。
开发态 `npm run dev``npm run dev:api-server` 会默认注入 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true`,因此密码登录在本地开发环境可直接注册未知手机号账号;生产环境仍按 `api-server` 配置默认关闭该开关。
@@ -61,7 +63,7 @@ spacetime sql <database> "SELECT * FROM puzzle_gallery_card_view LIMIT 1" --serv
本地 `npm run dev:spacetime` 发布模块时必须显式忽略仓库根目录的 `spacetime.json`,由脚本固定追加 `--no-config` 并使用命令参数里传入的数据库名和 `--server http://127.0.0.1:3101`。否则 CLI 可能把发布目标改写到配置文件里的其他数据库,导致 `dev:spacetime` 启动后又因发布失败自动退出,浏览器随后会在 `ws://127.0.0.1:3101/v1/database/.../subscribe` 看到连接拒绝。
本地 `spacetime` CLI / standalone 版本必须和 `server-rs/Cargo.toml` 里锁定的 `spacetimedb` 版本一致。若版本错配procedure 返回值可能在宿主侧触发 `Failed to BSATN deserialize procedure return value`api-server 最终表现为敲木鱼等创作动作的 `SpacetimeDB procedure 调用超时`。排障时先运行 `spacetime --version`,再对照 `server-rs/Cargo.toml``spacetimedb = "..."`;需要切版本时执行 `spacetime version install <version> && spacetime version use <version>`,然后重新启动 `npm run dev:spacetime`。当前 `scripts/dev.mjs` 会在启动和复用本地 SpacetimeDB 前写入并校验 `dev-spacetime-tool-version`,避免把旧 standalone 继续带进新一轮创作。
本地 `spacetime` CLI / standalone 版本必须和 `server-rs/Cargo.toml` 里锁定的 `spacetimedb` 版本一致;当前统一版本为 `2.3.0`。若版本错配procedure 返回值可能在宿主侧触发 `Failed to BSATN deserialize procedure return value`api-server 最终表现为敲木鱼等创作动作的 `SpacetimeDB procedure 调用超时`。排障时先运行 `spacetime --version`,再对照 `server-rs/Cargo.toml``spacetimedb = "..."`;需要切版本时执行 `spacetime version install <version> && spacetime version use <version>`,然后重新启动 `npm run dev:spacetime`。当前 `scripts/dev.mjs` 会在启动和复用本地 SpacetimeDB 前写入并校验 `dev-spacetime-tool-version`,避免把旧 standalone 继续带进新一轮创作。
本地 `.env``.env.local``.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查 RPG / 拼图 / 抓大鹅等 VectorEngine 生图链路时,确认 `VECTOR_ENGINE_BASE_URL``VECTOR_ENGINE_API_KEY``VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git。VectorEngine `gpt-image-2` 图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image``api-server` 只做配置、玩法编排、OSS / asset 持久化、计费和失败审计落库。开局 CG 故事板、首图、背景和图集都属于长耗时图片请求;后端默认会把 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 下限收口到 `1000000`,旧进程仍可能沿用重启前的短超时。若开局 CG 故事板在 `send()` 阶段失败且日志显示 `SendRequest`,先看同一 request_id 的 `request_body_bytes``reference_data_url_bytes``sourceChain``rootSource`;当前开局 CG 会把角色图与首幕背景图压到单边 768 的 JPEG 后再作为 generations `image` 数组发送,`/v1/images/generations` 使用默认 HTTP 协商,只有 multipart `/v1/images/edits` 单独强制 HTTP/1.1。

View File

@@ -1,5 +1,22 @@
import {randomBytes} from 'node:crypto';
import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from 'node:fs';
import {hostname, userInfo} from 'node:os';
import {dirname, resolve} from 'node:path';
import {createServer} from 'node:net';
const LINUX_DEV_PORT_RANGE_REGISTRY_ROOT = '/var/tmp/genarrative-dev-port-ranges';
const LINUX_DEV_PORT_RANGE_POOL_START = 10000;
const LINUX_DEV_PORT_RANGE_POOL_END = 39999;
const LINUX_DEV_PORT_RANGE_BLOCK_SIZE = 100;
function toListenHosts(host) {
if (host === '0.0.0.0') {
return ['0.0.0.0'];
@@ -21,6 +38,416 @@ export function normalizePort(value, fallback) {
return port;
}
export function parsePortRangeSpec(value) {
const spec = String(value ?? '').trim();
if (!spec) {
return null;
}
const match = /^(\d+)\s*[-:]\s*(\d+)$/u.exec(spec);
if (!match) {
throw new Error(`端口段格式错误: ${spec},应为 start-end 形式,如 10000-10099`);
}
const start = normalizePort(match[1], -1);
const end = normalizePort(match[2], -1);
if (start < 1024 || end < 1024 || start > end) {
throw new Error(`端口段无效: ${spec},端口必须在 1024-65535 且起始不大于结束`);
}
if (end - start + 1 < 4) {
throw new Error(`端口段至少需要 4 个端口: ${spec}`);
}
return {start, end, label: `${start}-${end}`};
}
function normalizePortRange(portRange) {
if (!portRange) {
return null;
}
if (typeof portRange === 'string') {
return parsePortRangeSpec(portRange);
}
return parsePortRangeSpec(`${portRange.start}-${portRange.end}`);
}
export function getLinuxDevPortRangeRegistryPaths(env = process.env) {
const registryRoot =
String(env.GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR ?? '').trim() ||
LINUX_DEV_PORT_RANGE_REGISTRY_ROOT;
return {
root: registryRoot,
registryPath: resolve(registryRoot, 'registry.json'),
lockPath: resolve(registryRoot, 'registry.lock'),
};
}
export function getLinuxDevPortRangeUsername(env = process.env) {
const candidates = [env.SUDO_USER, env.LOGNAME, env.USER, env.USERNAME];
for (const candidate of candidates) {
const value = String(candidate ?? '').trim();
if (value) {
return value;
}
}
try {
return userInfo().username;
} catch {
return 'unknown';
}
}
export function getLinuxDevPortRangeSpec(env = process.env) {
return parsePortRangeSpec(env.GENARRATIVE_DEV_PORT_RANGE);
}
export function mapDevPortsToPortRange(portRange) {
const normalizedRange = normalizePortRange(portRange);
if (!normalizedRange) {
return null;
}
return {
webPort: normalizedRange.start,
apiPort: normalizedRange.start + 1,
spacetimePort: normalizedRange.start + 2,
adminWebPort: normalizedRange.start + 3,
range: normalizedRange,
};
}
function ensureWorldWritableDirectory(path) {
mkdirSync(path, {recursive: true});
try {
chmodSync(path, 0o777);
} catch {
// 目录已存在时若无法改权限,也不影响当前进程继续写入。
}
}
function randomToken(byteLength = 8) {
return randomBytes(byteLength).toString('hex');
}
function readJsonFile(path, fallbackValue) {
if (!existsSync(path)) {
return fallbackValue;
}
const rawText = readFileSync(path, 'utf8').trim();
if (!rawText) {
return fallbackValue;
}
try {
return JSON.parse(rawText);
} catch (error) {
throw new Error(`系统级端口段记录文件损坏: ${path}; ${error.message}`);
}
}
function atomicWriteJsonFile(path, value) {
const targetDir = dirname(path);
ensureWorldWritableDirectory(targetDir);
const tempPath = resolve(
targetDir,
`.tmp.${process.pid}.${Date.now()}.${randomToken()}.json`,
);
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
encoding: 'utf8',
mode: 0o666,
});
try {
chmodSync(tempPath, 0o666);
} catch {
// 先写成功再说,后续 rename 之后还能补一次。
}
try {
renameSync(tempPath, path);
} catch (error) {
try {
rmSync(tempPath, {force: true});
} catch {
// ignore cleanup races
}
throw error;
}
try {
chmodSync(path, 0o666);
} catch {
// 忽略权限补写失败,记录已落盘即可。
}
}
function isProcessAlive(pid) {
if (!Number.isInteger(pid) || pid <= 0) {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error?.code === 'EPERM';
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getDefaultPortRangeRegistry() {
return {
version: 4,
updatedAt: '',
allocations: {},
};
}
function readLinuxPortRangeRegistry(registryPath) {
const registry = readJsonFile(registryPath, getDefaultPortRangeRegistry());
if (!registry || typeof registry !== 'object' || Array.isArray(registry)) {
throw new Error(`系统级端口段记录文件格式错误: ${registryPath}`);
}
if (!registry.allocations || typeof registry.allocations !== 'object') {
registry.allocations = {};
}
const normalizedAllocations = {};
for (const [key, record] of Object.entries(registry.allocations)) {
if (!record || typeof record !== 'object' || Array.isArray(record)) {
continue;
}
const username = String(record.username ?? key ?? '').trim();
const range = safeNormalizePortRange(record.range);
if (!username || !range) {
continue;
}
normalizedAllocations[username] = {
username,
range,
claimedAt: String(record.claimedAt ?? '').trim(),
updatedAt: String(record.updatedAt ?? record.claimedAt ?? '').trim(),
source: String(record.source ?? 'auto').trim() || 'auto',
};
}
registry.version = 4;
registry.allocations = normalizedAllocations;
return registry;
}
function safeNormalizePortRange(portRange) {
try {
return normalizePortRange(portRange);
} catch {
return null;
}
}
function rangesOverlap(left, right) {
return left.start <= right.end && right.start <= left.end;
}
function findRangeConflict(registry, portRange, excludingUsername = '') {
const rangeToCheck = safeNormalizePortRange(portRange);
if (!rangeToCheck) {
return null;
}
for (const [username, record] of Object.entries(registry.allocations ?? {})) {
if (username === excludingUsername) {
continue;
}
const recordRange = safeNormalizePortRange(record?.range);
if (!recordRange || !rangesOverlap(recordRange, rangeToCheck)) {
continue;
}
return {username, record, range: recordRange};
}
return null;
}
function nextLinuxPortRangeCandidate(registry) {
for (
let start = LINUX_DEV_PORT_RANGE_POOL_START;
start <= LINUX_DEV_PORT_RANGE_POOL_END;
start += LINUX_DEV_PORT_RANGE_BLOCK_SIZE
) {
const end = start + LINUX_DEV_PORT_RANGE_BLOCK_SIZE - 1;
if (end > LINUX_DEV_PORT_RANGE_POOL_END) {
break;
}
const label = `${start}-${end}`;
if (!findRangeConflict(registry, {start, end, label})) {
return {start, end, label};
}
}
return null;
}
async function withLinuxPortRangeRegistryLock(lockPath, action) {
ensureWorldWritableDirectory(dirname(lockPath));
while (true) {
try {
writeFileSync(
lockPath,
JSON.stringify(
{
pid: process.pid,
hostname: hostname(),
acquiredAt: new Date().toISOString(),
},
null,
2,
),
{encoding: 'utf8', flag: 'wx', mode: 0o666},
);
break;
} catch (error) {
if (error?.code !== 'EEXIST') {
throw error;
}
let lockRecord = null;
try {
lockRecord = readJsonFile(lockPath, null);
} catch {
lockRecord = null;
}
if (!lockRecord || !isProcessAlive(lockRecord.pid)) {
try {
rmSync(lockPath, {force: true});
} catch {
// ignore cleanup races
}
continue;
}
await sleep(50);
}
}
try {
return await action();
} finally {
try {
rmSync(lockPath, {force: true});
} catch {
// ignore cleanup races
}
}
}
export async function reserveLinuxDevPortRange({
env = process.env,
username = getLinuxDevPortRangeUsername(env),
requestedRange = getLinuxDevPortRangeSpec(env),
registryPath = getLinuxDevPortRangeRegistryPaths(env).registryPath,
lockPath = getLinuxDevPortRangeRegistryPaths(env).lockPath,
} = {}) {
if (process.platform !== 'linux') {
return null;
}
const normalizedRequestedRange = normalizePortRange(requestedRange);
const now = new Date().toISOString();
return await withLinuxPortRangeRegistryLock(lockPath, async () => {
const registry = readLinuxPortRangeRegistry(registryPath);
const current = registry.allocations[username];
if (current) {
registry.updatedAt = now;
registry.allocations[username] = {
...current,
username,
range: current.range,
updatedAt: now,
};
atomicWriteJsonFile(registryPath, registry);
return registry.allocations[username];
}
const rangeToUse = normalizedRequestedRange || nextLinuxPortRangeCandidate(registry);
if (!rangeToUse) {
throw new Error(
`无法为 Linux dev 分配端口段,范围 ${LINUX_DEV_PORT_RANGE_POOL_START}-${LINUX_DEV_PORT_RANGE_POOL_END} 已耗尽`,
);
}
const conflict = findRangeConflict(registry, rangeToUse.label, username);
if (conflict) {
if (conflict.range?.label === rangeToUse.label) {
throw new Error(
`Linux 端口段 ${rangeToUse.label} 已被用户 ${conflict.username} 占用,请换一段再启动`,
);
}
throw new Error(
`Linux 端口段 ${rangeToUse.label} 已与用户 ${conflict.username} 的端口段 ${conflict.range?.label} 重叠,请换一段再启动`,
);
}
registry.allocations[username] = {
username,
range: rangeToUse,
source: normalizedRequestedRange ? 'manual' : 'auto',
claimedAt: now,
updatedAt: now,
};
registry.updatedAt = now;
atomicWriteJsonFile(registryPath, registry);
return registry.allocations[username];
});
}
export async function releaseLinuxDevPortRange({
env = process.env,
username = getLinuxDevPortRangeUsername(env),
registryPath = getLinuxDevPortRangeRegistryPaths(env).registryPath,
lockPath = getLinuxDevPortRangeRegistryPaths(env).lockPath,
} = {}) {
if (process.platform !== 'linux') {
return false;
}
return await withLinuxPortRangeRegistryLock(lockPath, async () => {
const registry = readLinuxPortRangeRegistry(registryPath);
if (!registry.allocations[username]) {
return false;
}
delete registry.allocations[username];
registry.updatedAt = new Date().toISOString();
atomicWriteJsonFile(registryPath, registry);
return true;
});
}
export async function isPortAvailable({host, port}) {
if (port === 0) {
return true;
@@ -46,16 +473,45 @@ export async function isPortAvailable({host, port}) {
return true;
}
export async function findAvailablePort({host, preferredPort, reservedPorts = new Set(), maxAttempts = 200}) {
export async function findAvailablePort({
host,
preferredPort,
reservedPorts = new Set(),
maxAttempts = null,
portRange = null,
}) {
const range = normalizePortRange(portRange);
const startPort = normalizePort(preferredPort, 0);
if (startPort === 0 && range) {
return await findAvailablePort({
host,
preferredPort: range.start,
reservedPorts,
maxAttempts: range.end - range.start,
portRange: range,
});
}
if (startPort === 0) {
return await reserveEphemeralPort(host, reservedPorts);
}
for (let offset = 0; offset <= maxAttempts; offset += 1) {
if (range && (startPort < range.start || startPort > range.end)) {
throw new Error(`端口 ${startPort} 不在允许端口段 ${range.label}`);
}
const boundedAttempts = range
? Number.isFinite(maxAttempts)
? Math.min(Math.max(0, maxAttempts), range.end - startPort)
: range.end - startPort
: Number.isFinite(maxAttempts)
? Math.max(0, maxAttempts)
: 200;
for (let offset = 0; offset <= boundedAttempts; offset += 1) {
const candidate = startPort + offset;
if (candidate > 65535) {
if (candidate > 65535 || (range && candidate > range.end)) {
break;
}
@@ -68,7 +524,8 @@ export async function findAvailablePort({host, preferredPort, reservedPorts = ne
}
}
throw new Error(`无法从 ${host}:${startPort} 开始找到可用端口`);
const rangeHint = range ? `,允许端口段 ${range.label}` : '';
throw new Error(`无法从 ${host}:${startPort} 开始找到可用端口${rangeHint}`);
}
async function reserveEphemeralPort(host, reservedPorts) {
@@ -107,6 +564,7 @@ export async function resolveDevStackPorts(config) {
host: portConfig.host,
preferredPort: portConfig.preferredPort,
reservedPorts,
portRange: portConfig.portRange,
});
reservedPorts.add(resolvedPort);
result[name] = resolvedPort;

View File

@@ -1,7 +1,13 @@
import {createServer} from 'node:net';
import {mkdtempSync, readFileSync, rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {describe, expect, it} from 'vitest';
import {
findAvailablePort,
mapDevPortsToPortRange,
parsePortRangeSpec,
reserveLinuxDevPortRange,
resolveDevStackPorts,
} from './dev-stack-port-utils.mjs';
@@ -18,6 +24,20 @@ function reservePort(port) {
}
describe('dev stack port utils', () => {
it('解析端口段并映射到四个 dev 端口', () => {
expect(parsePortRangeSpec('10000-10099')).toEqual({
start: 10000,
end: 10099,
label: '10000-10099',
});
expect(mapDevPortsToPortRange('10000-10099')).toMatchObject({
webPort: 10000,
apiPort: 10001,
spacetimePort: 10002,
adminWebPort: 10003,
});
});
it('使用端口可用性检查为被占用端口寻找后续可用端口', async () => {
const firstServer = await reservePort(0);
const firstPort = firstServer.address().port;
@@ -38,6 +58,16 @@ describe('dev stack port utils', () => {
}
});
it('端口查找不会越过 Linux 用户端口段', async () => {
await expect(
findAvailablePort({
host: '127.0.0.1',
preferredPort: 9999,
portRange: {start: 10000, end: 10099, label: '10000-10099'},
}),
).rejects.toThrow('不在允许端口段');
});
it('为 npm run dev 的所有后续流程解析互不冲突的端口', async () => {
const resolvedPorts = await resolveDevStackPorts({
spacetime: {host: '127.0.0.1', preferredPort: 0},
@@ -48,4 +78,191 @@ describe('dev stack port utils', () => {
expect(new Set(Object.values(resolvedPorts)).size).toBe(4);
});
it('端口段内会一直漂移到段尾,不会被默认 200 次尝试截断', async () => {
const rangeStart = 10000;
const rangeEnd = 10300;
const reservedPorts = new Set(
Array.from({length: 201}, (_, index) => rangeStart + index),
);
const availablePort = await findAvailablePort({
host: '127.0.0.1',
preferredPort: rangeStart,
reservedPorts,
portRange: {start: rangeStart, end: rangeEnd, label: `${rangeStart}-${rangeEnd}`},
});
expect(availablePort).toBeGreaterThan(rangeStart + 200);
expect(availablePort).toBeLessThanOrEqual(rangeEnd);
});
const linuxIt = process.platform === 'linux' ? it : it.skip;
linuxIt('Linux 未手动指定端口段时从 10000 开始按 100 端口块自动分配', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'genarrative-port-range-'));
const registryPath = join(tempRoot, 'registry.json');
const lockPath = join(tempRoot, 'registry.lock');
try {
const aliceAllocation = await reserveLinuxDevPortRange({
env: {
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
},
username: 'alice',
requestedRange: null,
registryPath,
lockPath,
});
const bobAllocation = await reserveLinuxDevPortRange({
env: {
USER: 'bob',
LOGNAME: 'bob',
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
},
username: 'bob',
requestedRange: null,
registryPath,
lockPath,
});
expect(aliceAllocation.range.label).toBe('10000-10099');
expect(aliceAllocation.source).toBe('auto');
expect(bobAllocation.range.label).toBe('10100-10199');
expect(bobAllocation.source).toBe('auto');
} finally {
rmSync(tempRoot, {recursive: true, force: true});
}
});
linuxIt('Linux 系统级端口段记录会阻止两个用户拿到同一段', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'genarrative-port-range-'));
const registryPath = join(tempRoot, 'registry.json');
const lockPath = join(tempRoot, 'registry.lock');
try {
const baseEnv = {
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
GENARRATIVE_DEV_PORT_RANGE: '10000-10099',
};
const aliceAllocation = await reserveLinuxDevPortRange({
env: baseEnv,
username: 'alice',
registryPath,
lockPath,
});
expect(aliceAllocation.range.label).toBe('10000-10099');
const registryAfterAlice = JSON.parse(readFileSync(registryPath, 'utf8'));
expect(registryAfterAlice.allocations.alice.range.label).toBe('10000-10099');
await expect(
reserveLinuxDevPortRange({
env: {
...baseEnv,
USER: 'bob',
LOGNAME: 'bob',
},
username: 'bob',
requestedRange: '10000-10099',
registryPath,
lockPath,
}),
).rejects.toThrow('已被用户 alice 占用');
const aliceReuse = await reserveLinuxDevPortRange({
env: {
...baseEnv,
USER: 'alice',
LOGNAME: 'alice',
},
username: 'alice',
registryPath,
lockPath,
});
expect(aliceReuse.range.label).toBe('10000-10099');
expect(aliceReuse.source).toBe('manual');
expect(
(
await reserveLinuxDevPortRange({
env: {
...baseEnv,
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_DEV_PORT_RANGE: '10100-10199',
},
username: 'alice',
requestedRange: '10100-10199',
registryPath,
lockPath,
})
).range.label,
).toBe('10000-10099');
const registryAfterReuse = JSON.parse(readFileSync(registryPath, 'utf8'));
expect(registryAfterReuse.allocations.alice.range.label).toBe('10000-10099');
const bobAllocation = await reserveLinuxDevPortRange({
env: {
...baseEnv,
USER: 'bob',
LOGNAME: 'bob',
GENARRATIVE_DEV_PORT_RANGE: '10100-10199',
},
username: 'bob',
requestedRange: '10100-10199',
registryPath,
lockPath,
});
expect(bobAllocation.range.label).toBe('10100-10199');
} finally {
rmSync(tempRoot, {recursive: true, force: true});
}
});
linuxIt('Linux 同一用户第二个 dev 会话会复用同一用户段并继续在段内漂移', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'genarrative-port-range-'));
const registryPath = join(tempRoot, 'registry.json');
const lockPath = join(tempRoot, 'registry.lock');
try {
const baseEnv = {
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
GENARRATIVE_DEV_PORT_RANGE: '10000-10099',
};
const first = await reserveLinuxDevPortRange({
env: baseEnv,
username: 'alice',
registryPath,
lockPath,
});
const second = await reserveLinuxDevPortRange({
env: baseEnv,
username: 'alice',
registryPath,
lockPath,
});
expect(first.range.label).toBe('10000-10099');
expect(second.range.label).toBe('10000-10099');
expect(second.username).toBe('alice');
expect(JSON.parse(readFileSync(registryPath, 'utf8')).allocations.alice.range.label).toBe(
'10000-10099',
);
} finally {
rmSync(tempRoot, {recursive: true, force: true});
}
});
});

View File

@@ -15,7 +15,11 @@ import {fileURLToPath} from 'node:url';
import {
formatPortDecision,
getLinuxDevPortRangeRegistryPaths,
getLinuxDevPortRangeUsername,
mapDevPortsToPortRange,
normalizePort,
reserveLinuxDevPortRange,
resolveDevStackPorts,
} from './dev-stack-port-utils.mjs';
import {
@@ -59,6 +63,7 @@ function usage() {
--spacetime-port <port> SpacetimeDB 端口
--spacetime-data-dir <path> SpacetimeDB 本地数据目录
--database <name> SpacetimeDB 数据库名
--port-range <start-end> Linux 用户端口段,手动指定示例 10000-10099默认自动从 10000-10099 起分配
--watch 文件改动后刷新/重启对应模块
--no-interactive 关闭交互式手动命令
@@ -109,6 +114,7 @@ function parseArgs(argv, baseEnv) {
spacetimePort: normalizePort(env.SPACETIME_PORT, 3101),
spacetimeDataDir: resolve(serverRsDir, '.spacetimedb/local/data'),
spacetimeServerUrl: String(env.GENARRATIVE_SPACETIME_SERVER_URL ?? '').trim(),
portRangeSpec: String(env.GENARRATIVE_DEV_PORT_RANGE ?? '').trim(),
database:
readLocalSpacetimeDatabase() ||
String(env.GENARRATIVE_SPACETIME_DATABASE ?? '').trim() ||
@@ -184,6 +190,10 @@ function parseArgs(argv, baseEnv) {
options.database = readValue();
explicitOptions.add('database');
break;
case '--port-range':
options.portRangeSpec = readValue();
explicitOptions.add('portRangeSpec');
break;
case '--log':
options.apiLog = readValue();
break;
@@ -549,6 +559,8 @@ class DevRunner {
adminWebTargetHost: resolveClientHost(options.adminWebHost),
spacetimeServer: initialSpacetimeServer,
apiTarget: `http://${resolveClientHost(options.apiHost)}:${options.apiPort}`,
portRange: null,
portRangeReservation: null,
};
this.services = new Map();
this.watchers = [];
@@ -572,12 +584,57 @@ class DevRunner {
ensureSpacetimeToolVersionMatchesWorkspace();
}
await this.prepareLinuxPortRange(command);
await this.tryReuseExistingSpacetime(command);
await this.resolvePorts(command);
this.registerServices();
this.printSummary(command);
}
async prepareLinuxPortRange(command) {
if (process.platform !== 'linux') {
return;
}
const requestedRange = String(this.options.portRangeSpec ?? '').trim();
const allocation = await reserveLinuxDevPortRange({
env: {
...this.baseEnv,
GENARRATIVE_DEV_PORT_RANGE: requestedRange,
},
username: getLinuxDevPortRangeUsername(this.baseEnv),
});
if (!allocation) {
return;
}
this.state.portRangeReservation = allocation;
this.state.portRange = allocation.range;
this.baseEnv.GENARRATIVE_DEV_PORT_RANGE = allocation.range.label;
const mappedPorts = mapDevPortsToPortRange(allocation.range);
if (!mappedPorts) {
return;
}
if (!this.explicitOptions.has('webPort')) {
this.options.webPort = mappedPorts.webPort;
}
if (!this.explicitOptions.has('apiPort')) {
this.options.apiPort = mappedPorts.apiPort;
}
if (!this.explicitOptions.has('spacetimePort')) {
this.options.spacetimePort = mappedPorts.spacetimePort;
}
if (!this.explicitOptions.has('adminWebPort')) {
this.options.adminWebPort = mappedPorts.adminWebPort;
}
this.state.spacetimeServer = `http://${this.options.spacetimeHost}:${this.options.spacetimePort}`;
this.state.apiTarget = `http://${this.state.apiTargetHost}:${this.options.apiPort}`;
}
shouldValidateSpacetimeToolVersion(command) {
if (command === 'spacetime') {
return true;
@@ -626,6 +683,10 @@ class DevRunner {
]);
for (const candidate of candidates) {
if (!this.isCandidateSpacetimeWithinAssignedRange(candidate)) {
continue;
}
const pingUrl = buildUrl(candidate, '/v1/ping');
if (!pingUrl || !(await isHttpReady(pingUrl))) {
continue;
@@ -653,6 +714,21 @@ class DevRunner {
);
}
isCandidateSpacetimeWithinAssignedRange(candidateUrl) {
const range = this.state.portRange;
if (!range) {
return true;
}
try {
const url = new URL(candidateUrl);
const port = Number(url.port);
return Number.isInteger(port) && port >= range.start && port <= range.end;
} catch {
return false;
}
}
async resolvePorts(command) {
const {options} = this;
const portConfig = {};
@@ -662,6 +738,7 @@ class DevRunner {
portConfig.spacetime = {
host: options.spacetimeHost,
preferredPort: options.spacetimePort,
portRange: this.state.portRange,
};
}
}
@@ -670,6 +747,7 @@ class DevRunner {
portConfig.api = {
host: options.apiHost,
preferredPort: options.apiPort,
portRange: this.state.portRange,
};
}
@@ -677,6 +755,7 @@ class DevRunner {
portConfig.web = {
host: options.webHost,
preferredPort: options.webPort,
portRange: this.state.portRange,
};
}
@@ -684,6 +763,7 @@ class DevRunner {
portConfig.adminWeb = {
host: options.adminWebHost,
preferredPort: options.adminWebPort,
portRange: this.state.portRange,
};
}
@@ -747,6 +827,15 @@ class DevRunner {
console.log(`[dev] repo: ${repoRoot}`);
console.log(`[dev] command: ${command}`);
console.log(`[dev] watch: ${options.watch ? 'on' : 'off'}`);
if (state.portRange) {
const owner =
state.portRangeReservation?.username ||
getLinuxDevPortRangeUsername(this.baseEnv);
console.log(`[dev] port-range: ${state.portRange.label} (${owner})`);
console.log(
`[dev] port-range-registry: ${getLinuxDevPortRangeRegistryPaths(this.baseEnv).registryPath}`,
);
}
console.log(`[dev] web: http://127.0.0.1:${options.webPort}`);
console.log(`[dev] admin web: http://${state.adminWebTargetHost}:${options.adminWebPort}/admin/`);
console.log(`[dev] api-server: ${state.apiTarget}`);

View File

@@ -35,6 +35,8 @@ function workspaceSpacetimeVersionForTest() {
}
describe('dev scheduler argument routing', () => {
const linuxTest = process.platform === 'linux' ? test : test.skip;
test('完整 dev 栈覆盖前端代理到本次解析出的 api-server 地址', () => {
const {command, explicitOptions, options} = parseArgs([], {
GENARRATIVE_API_PORT: '8090',
@@ -88,6 +90,67 @@ describe('dev scheduler argument routing', () => {
'http://127.0.0.1:3100',
);
});
linuxTest('Linux 启动时按系统级端口段映射四个 dev 端口', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-dev-port-range-'));
try {
const {command, explicitOptions, options} = parseArgs([], {
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir,
});
const runner = new DevRunner(options, {
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir,
}, explicitOptions);
await runner.prepareLinuxPortRange(command);
expect(runner.state.portRange.label).toBe('22000-22099');
expect(runner.options.webPort).toBe(22000);
expect(runner.options.apiPort).toBe(22001);
expect(runner.options.spacetimePort).toBe(22002);
expect(runner.options.adminWebPort).toBe(22003);
expect(runner.state.apiTarget).toBe('http://127.0.0.1:22001');
expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:22002');
} finally {
rmSync(tempDir, {recursive: true, force: true});
}
});
test('Windows 仍沿用原有端口解析,不启用 Linux 端口段登记', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'win32',
});
try {
const {command, explicitOptions, options} = parseArgs([], {
USER: 'alice',
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
});
const runner = new DevRunner(options, {
USER: 'alice',
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
}, explicitOptions);
await runner.prepareLinuxPortRange(command);
expect(runner.state.portRange).toBeNull();
expect(runner.options.webPort).toBe(3000);
expect(runner.options.apiPort).toBe(8082);
expect(runner.options.spacetimePort).toBe(3101);
expect(runner.options.adminWebPort).toBe(3102);
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform);
}
}
});
});
describe('dev scheduler api-server env', () => {
@@ -252,18 +315,18 @@ describe('dev scheduler watch routing', () => {
describe('dev scheduler spacetime refresh', () => {
test('解析 spacetime --version 输出里的 tool version', () => {
const version = parseSpacetimeToolVersion(`
A new version of SpacetimeDB is available: v2.2.0 (current: v2.1.0)
spacetimedb tool version 2.2.0; spacetimedb-lib version 2.2.0;
A new version of SpacetimeDB is available: v2.3.0 (current: v2.2.0)
spacetimedb tool version 2.3.0; spacetimedb-lib version 2.3.0;
`);
expect(version).toBe('2.2.0');
expect(version).toBe('2.3.0');
});
test('本机 spacetime 版本和 workspace 锁定版本不一致时直接报清楚', () => {
expect(() =>
assertSpacetimeToolVersionMatchesWorkspace({
toolVersion: '2.1.0',
workspaceVersion: '2.2.0',
workspaceVersion: '2.3.0',
}),
).toThrow('procedure 返回值 BSATN 反序列化失败');
});

64
server-rs/Cargo.lock generated
View File

@@ -1310,7 +1310,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.3",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -2605,7 +2605,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.3",
"socket2 0.5.10",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -2642,7 +2642,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.3",
"socket2 0.5.10",
"tracing",
"windows-sys 0.52.0",
]
@@ -3423,9 +3423,9 @@ dependencies = [
[[package]]
name = "spacetimedb"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1306cc3a9ed9c89f43b263614a529357cc53a067e3d06c1cbb485e3b577b118b"
checksum = "62aa9a940d32178e4afa7a14c9bbda76685d71d5487798905e0139b807182092"
dependencies = [
"anyhow",
"bytemuck",
@@ -3446,9 +3446,9 @@ dependencies = [
[[package]]
name = "spacetimedb-bindings-macro"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51567ec01cd323438a00c134c16f26ffcde5f9dbe6a42a52e54578285bf49d73"
checksum = "5feb5d55f04f3209764d9b94949226708a4a8578e92ac5c32abfd31dbdfc928c"
dependencies = [
"heck 0.4.1",
"humantime",
@@ -3460,18 +3460,18 @@ dependencies = [
[[package]]
name = "spacetimedb-bindings-sys"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b40fa1bea26664085febe2b4455568c8b47dea2cb0245406b27e30963df2ba1"
checksum = "28f9c900c9371fd7e84d34b8cb2bf90562060dc2473ae9c44e970d4026e7d7d9"
dependencies = [
"spacetimedb-primitives",
]
[[package]]
name = "spacetimedb-client-api-messages"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc9eeb20a555bad07029cbee4efe3a305cb5c1e40e21a07cbbbbed16a106014"
checksum = "349296ad43e6ecdced74ad8b3fd2c6abbdbb40cdbd06ac329c0726c6b911fa73"
dependencies = [
"bytes",
"bytestring",
@@ -3491,9 +3491,9 @@ dependencies = [
[[package]]
name = "spacetimedb-data-structures"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "748fd5850a757823c5b8948065d9e4dc5092968a051aa3f34f170e91d95e493b"
checksum = "8b86ed7c567d723378a405317e30413293cc0bc9e2aac2f7843580d744b43c31"
dependencies = [
"ahash",
"crossbeam-queue",
@@ -3506,9 +3506,9 @@ dependencies = [
[[package]]
name = "spacetimedb-lib"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5612611d09d358f535438275d2a0d6a5e2fa56fa583dcfdbeddd623974df1d5e"
checksum = "a15a98adf6f030e8188df4b038e140a54771e6eeb50ad05a6c3e46939b8de853"
dependencies = [
"anyhow",
"bitflags 2.11.1",
@@ -3530,9 +3530,9 @@ dependencies = [
[[package]]
name = "spacetimedb-memory-usage"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c3a0d08fc5d8688a47e3ffcb803275519663b7ea1fba7ad25e608182de4ec6d"
checksum = "c68aa8ed30c15a1d665bf3a8c689955508ce75ca784068ec0232b4cdd511b4c8"
dependencies = [
"decorum",
"ethnum",
@@ -3540,9 +3540,9 @@ dependencies = [
[[package]]
name = "spacetimedb-metrics"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2d647201339aa17ba438a07463e96ed64ba214fb0c182588e262b055efa7f3"
checksum = "f8455b0a92dd632757f7e7c22d5e438aa33da5f48d14483e5ee79dfc5468a4db"
dependencies = [
"arrayvec",
"itertools",
@@ -3552,9 +3552,9 @@ dependencies = [
[[package]]
name = "spacetimedb-primitives"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b668b51e7318207ae7eebcd4cae0c5d43bf713e7f229ac309ea2614a486ffde"
checksum = "b20cc4bf97377f1dce9e75b2f6ce94bc5c7c2a243040a7a2016ac5cdb002793d"
dependencies = [
"bitflags 2.11.1",
"either",
@@ -3566,18 +3566,18 @@ dependencies = [
[[package]]
name = "spacetimedb-query-builder"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0186b1a2b3bf25bdd0f2676b61801fd754013ca6a58e1e24cc5148945388bc9d"
checksum = "afa17878dbc23b4bfc06a45165c7afd34c8d29bba6dfde81625840c11380abce"
dependencies = [
"spacetimedb-lib",
]
[[package]]
name = "spacetimedb-sats"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11780ed69f178bf3784b7599da5171450e4b7ac6fd66b79e2e1861c867cef1a6"
checksum = "74216db354eab5cefad9572a350654761495968478e83e51ef2c530cdf6cb1d4"
dependencies = [
"anyhow",
"arrayvec",
@@ -3608,9 +3608,9 @@ dependencies = [
[[package]]
name = "spacetimedb-schema"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4e9f8aa596e0e7034f0c8b3649d3fa3cc7bde340761519c3a3c60f10ec8888"
checksum = "ff3ff36f901f6875907ff1df2b610fc396937b88f6793dfa04b0d9f298d74946"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -3639,9 +3639,9 @@ dependencies = [
[[package]]
name = "spacetimedb-sdk"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41e82f20034b8aaeaa081871b07895aab45be1f0fc35e114ab64ae8e7e5c1a54"
checksum = "26897e31aa58acd6cd0bcf12cf56a4ecd0cb4fc48053478626729e868f042e54"
dependencies = [
"anymap3",
"base64 0.21.7",
@@ -3671,9 +3671,9 @@ dependencies = [
[[package]]
name = "spacetimedb-sql-parser"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec5c77a2d4e3f42ede59598c56cb81a0fe54fd1974e2707f7140d1d5f41d08a7"
checksum = "19428d4ddc8cf1eb34e58715ed512820ee9a3de187a89f88f80e18e914a086ae"
dependencies = [
"derive_more",
"spacetimedb-lib",
@@ -4569,7 +4569,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]

View File

@@ -115,9 +115,9 @@ serde_urlencoded = "0.7"
sha1 = "0.10"
sha2 = "0.10"
socket2 = "0.6"
spacetimedb = "2.2.0"
spacetimedb-sdk = "2.2.0"
spacetimedb-lib = { version = "2.2.0", default-features = false }
spacetimedb = "2.3.0"
spacetimedb-sdk = "2.3.0"
spacetimedb-lib = { version = "2.3.0", default-features = false }
time = "0.3"
tokio = "1"
tokio-stream = "0.1"

View File

@@ -1,7 +1,7 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
// This was generated using spacetimedb cli version 2.2.0 (commit eb11e2f5c41dce6979715ad407996270d61329f6).
// This was generated using spacetimedb cli version 2.3.0 (commit aa73d1c35b4b346b98eeba10a3d756b4ae72162f).
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};

View File

@@ -47,10 +47,8 @@ pub trait accept_quest {
&self,
input: QuestRecordInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl accept_quest for super::RemoteReducers {
&self,
input: QuestRecordInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait acknowledge_quest_completion {
&self,
input: QuestCompletionAckInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl acknowledge_quest_completion for super::RemoteReducers {
&self,
input: QuestCompletionAckInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -50,10 +50,8 @@ pub trait apply_chapter_progression_ledger_entry {
&self,
input: ChapterProgressionLedgerInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -63,10 +61,8 @@ impl apply_chapter_progression_ledger_entry for super::RemoteReducers {
&self,
input: ChapterProgressionLedgerInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(

View File

@@ -47,10 +47,8 @@ pub trait apply_inventory_mutation {
&self,
input: InventoryMutationInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl apply_inventory_mutation for super::RemoteReducers {
&self,
input: InventoryMutationInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait apply_quest_signal {
&self,
input: QuestSignalApplyInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl apply_quest_signal for super::RemoteReducers {
&self,
input: QuestSignalApplyInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait begin_story_session {
&self,
input: StorySessionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl begin_story_session for super::RemoteReducers {
&self,
input: StorySessionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait bind_asset_object_to_entity {
&self,
input: AssetEntityBindingInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl bind_asset_object_to_entity for super::RemoteReducers {
&self,
input: AssetEntityBindingInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait confirm_asset_object {
&self,
input: AssetObjectUpsertInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl confirm_asset_object for super::RemoteReducers {
&self,
input: AssetObjectUpsertInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait continue_story {
&self,
input: StoryContinueInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl continue_story for super::RemoteReducers {
&self,
input: StoryContinueInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait create_ai_task {
&self,
input: AiTaskCreateInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl create_ai_task for super::RemoteReducers {
&self,
input: AiTaskCreateInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait create_battle_state {
&self,
input: BattleStateInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl create_battle_state for super::RemoteReducers {
&self,
input: BattleStateInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -50,10 +50,8 @@ pub trait ensure_analytics_date_dimension_for_date {
&self,
input: AnalyticsDateDimensionEnsureInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -63,10 +61,8 @@ impl ensure_analytics_date_dimension_for_date for super::RemoteReducers {
&self,
input: AnalyticsDateDimensionEnsureInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(

View File

@@ -50,10 +50,8 @@ pub trait grant_player_progression_experience {
&self,
input: PlayerProgressionGrantInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -63,10 +61,8 @@ impl grant_player_progression_experience for super::RemoteReducers {
&self,
input: PlayerProgressionGrantInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -50,10 +50,8 @@ pub trait publish_custom_world_profile {
&self,
input: CustomWorldProfilePublishInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -63,10 +61,8 @@ impl publish_custom_world_profile for super::RemoteReducers {
&self,
input: CustomWorldProfilePublishInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait resolve_combat_action {
&self,
input: ResolveCombatActionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl resolve_combat_action for super::RemoteReducers {
&self,
input: ResolveCombatActionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait resolve_npc_interaction {
&self,
input: ResolveNpcInteractionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl resolve_npc_interaction for super::RemoteReducers {
&self,
input: ResolveNpcInteractionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait resolve_npc_social_action {
&self,
input: ResolveNpcSocialActionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl resolve_npc_social_action for super::RemoteReducers {
&self,
input: ResolveNpcSocialActionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait resolve_treasure_interaction {
&self,
input: TreasureResolveInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl resolve_treasure_interaction for super::RemoteReducers {
&self,
input: TreasureResolveInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -50,10 +50,8 @@ pub trait seed_analytics_date_dimensions {
&self,
input: AnalyticsDateDimensionSeedInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -63,10 +61,8 @@ impl seed_analytics_date_dimensions for super::RemoteReducers {
&self,
input: AnalyticsDateDimensionSeedInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait start_ai_task {
&self,
input: AiTaskStartInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl start_ai_task for super::RemoteReducers {
&self,
input: AiTaskStartInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait start_ai_task_stage {
&self,
input: AiTaskStageStartInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl start_ai_task_stage for super::RemoteReducers {
&self,
input: AiTaskStageStartInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait turn_in_quest {
&self,
input: QuestTurnInInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl turn_in_quest for super::RemoteReducers {
&self,
input: QuestTurnInInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -50,10 +50,8 @@ pub trait unpublish_custom_world_profile {
&self,
input: CustomWorldProfileUnpublishInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -63,10 +61,8 @@ impl unpublish_custom_world_profile for super::RemoteReducers {
&self,
input: CustomWorldProfileUnpublishInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait upsert_chapter_progression {
&self,
input: ChapterProgressionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl upsert_chapter_progression for super::RemoteReducers {
&self,
input: ChapterProgressionInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -50,10 +50,8 @@ pub trait upsert_custom_world_profile {
&self,
input: CustomWorldProfileUpsertInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -63,10 +61,8 @@ impl upsert_custom_world_profile for super::RemoteReducers {
&self,
input: CustomWorldProfileUpsertInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -47,10 +47,8 @@ pub trait upsert_npc_state {
&self,
input: NpcStateUpsertInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -60,10 +58,8 @@ impl upsert_npc_state for super::RemoteReducers {
&self,
input: NpcStateUpsertInput,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp

View File

@@ -7,6 +7,8 @@ import {defineConfig, loadEnv} from 'vite';
export default defineConfig(({mode}) => {
const env = loadEnv(mode, __dirname, '');
const ignoredWatchGlobs = [
'**/.git/**',
'**/.worktrees/**',
'**/dist/**',
'**/dist_check/**',
'**/dist_check_final/**',
@@ -22,6 +24,7 @@ export default defineConfig(({mode}) => {
'**/media/**',
'**/scripts/**',
'**/server-rs/**',
'**/server-rs/target/**',
'**/*.test.ts',
'**/*.test.tsx',
'**/*.spec.ts',