dscs-server 0.0.1 → 0.1.1
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/README.md +14 -0
- package/assets/favicon.svg +8 -0
- package/assets/logo-dark.svg +9 -0
- package/dist/db.js +72 -1
- package/dist/http.js +363 -112
- package/dist/index.js +0 -0
- package/dist/sessions.js +160 -9
- package/dist/shared/batches.js +24 -8
- package/dist/shared/blob-batch.js +85 -0
- package/dist/shared/chain.js +14 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/protocol.js +2 -2
- package/dist/shared/sealed-frame.js +50 -13
- package/package.json +24 -2
package/README.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
DSH Session CloudSync 的自部署服务端。
|
|
4
4
|
|
|
5
|
+
当前版本:**0.1.0,v2-only**(`PROTOCOL_VERSION = 3`)。设备 API 只有 `/v2/*`,
|
|
6
|
+
旧的 `/v1/*` 已移除;0.0.x 插件不兼容,需要配套的 0.1.0 插件。
|
|
7
|
+
|
|
8
|
+
## 能力
|
|
9
|
+
|
|
10
|
+
- `/v2/sessions` 全量快照、`/v2/changes` 增量流;
|
|
11
|
+
- 会话摘要、ETag、批次条件分页;
|
|
12
|
+
- 带 `Idempotency-Key` 的 append;
|
|
13
|
+
- `POST /v2/blobs/batch` / `POST /v2/blobs/batch/read` 批量 blob(8 MiB / 64 条);
|
|
14
|
+
- zstd 封帧(加密前压缩)与服务端压缩状态聚合;
|
|
15
|
+
- Vault 租约、append、cursor、stream。
|
|
16
|
+
|
|
5
17
|
## 安装
|
|
6
18
|
|
|
7
19
|
```sh
|
|
@@ -19,6 +31,8 @@ Linux/macOS 也可以使用仓库的 `scripts/install.sh`;Windows 使用 `scri
|
|
|
19
31
|
- 默认只监听 `127.0.0.1`,公网部署请放在 TLS 反代后;
|
|
20
32
|
- 数据目录包含 SQLite 与 blob 文件,升级前请备份;
|
|
21
33
|
- 服务端不持有明文会话内容,只存不透明密文;
|
|
34
|
+
- 0.0.1 数据目录升级到 0.1.0 会迁移到 schema 4,但旧插件仍会 404;
|
|
35
|
+
- 已有未压缩会话不会自动压缩;需要清空数据目录后由 0.1.0 插件全量重传;
|
|
22
36
|
- 协议说明见仓库 `docs/architecture/PROTOCOL.md`。
|
|
23
37
|
|
|
24
38
|
## 许可证
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" role="img" aria-label="dsh-session-cloudsync">
|
|
2
|
+
<g transform="translate(64 0) scale(-1 1)">
|
|
3
|
+
<path d="M18 16 H46 A7 7 0 0 1 46 30 H18 A7 7 0 0 0 18 44 H32"
|
|
4
|
+
stroke="#2D6CDF" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<path d="M38 44 H46" stroke="#2D6CDF" stroke-width="8" stroke-linecap="round"/>
|
|
6
|
+
<circle cx="35" cy="44" r="3.4" fill="#E6E8EB"/>
|
|
7
|
+
</g>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" role="img" aria-label="dsh-session-cloudsync">
|
|
2
|
+
<title>dsh-session-cloudsync</title>
|
|
3
|
+
<g transform="translate(64 0) scale(-1 1)">
|
|
4
|
+
<path d="M18 16 H46 A7 7 0 0 1 46 30 H18 A7 7 0 0 0 18 44 H32"
|
|
5
|
+
stroke="#E6E8EB" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
+
<path d="M38 44 H46" stroke="#E6E8EB" stroke-width="7" stroke-linecap="round"/>
|
|
7
|
+
<circle cx="35" cy="44" r="3.2" fill="#6EA8FF"/>
|
|
8
|
+
</g>
|
|
9
|
+
</svg>
|
package/dist/db.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdirSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
5
|
export const DEFAULT_DATASET = 'default';
|
|
6
|
-
export const SERVER_SCHEMA_VERSION =
|
|
6
|
+
export const SERVER_SCHEMA_VERSION = 4;
|
|
7
7
|
const SCHEMA_VERSION = SERVER_SCHEMA_VERSION;
|
|
8
8
|
const SCHEMA = `
|
|
9
9
|
CREATE TABLE IF NOT EXISTS meta (
|
|
@@ -35,6 +35,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
35
35
|
revision INTEGER NOT NULL,
|
|
36
36
|
updated_at INTEGER NOT NULL,
|
|
37
37
|
deleted_at INTEGER,
|
|
38
|
+
compression TEXT NOT NULL DEFAULT 'none',
|
|
39
|
+
stored_bytes INTEGER NOT NULL DEFAULT 0,
|
|
38
40
|
PRIMARY KEY (dataset, session_id)
|
|
39
41
|
);
|
|
40
42
|
|
|
@@ -45,6 +47,17 @@ CREATE TABLE IF NOT EXISTS blobs (
|
|
|
45
47
|
created_at INTEGER NOT NULL
|
|
46
48
|
);
|
|
47
49
|
|
|
50
|
+
CREATE TABLE IF NOT EXISTS append_requests (
|
|
51
|
+
dataset TEXT NOT NULL,
|
|
52
|
+
device_id TEXT NOT NULL,
|
|
53
|
+
session_id TEXT NOT NULL,
|
|
54
|
+
idempotency_key TEXT NOT NULL,
|
|
55
|
+
request_hash TEXT NOT NULL,
|
|
56
|
+
result_json TEXT NOT NULL,
|
|
57
|
+
created_at INTEGER NOT NULL,
|
|
58
|
+
PRIMARY KEY (dataset, device_id, session_id, idempotency_key)
|
|
59
|
+
);
|
|
60
|
+
|
|
48
61
|
CREATE TABLE IF NOT EXISTS leases (
|
|
49
62
|
dataset TEXT NOT NULL,
|
|
50
63
|
resource TEXT NOT NULL,
|
|
@@ -113,12 +126,59 @@ function backupBeforeMigration(handle, dataDir, version) {
|
|
|
113
126
|
const target = join(dataDir, `db.sqlite.bak-v${version}-${Date.now()}`);
|
|
114
127
|
handle.exec(`VACUUM INTO '${target.replaceAll("'", "''")}'`);
|
|
115
128
|
}
|
|
129
|
+
function backfillSessionStats(handle) {
|
|
130
|
+
const rows = handle.prepare('SELECT session_id, manifest_json FROM sessions').all();
|
|
131
|
+
const update = handle.prepare('UPDATE sessions SET compression = ?, stored_bytes = ? WHERE dataset = ? AND session_id = ?');
|
|
132
|
+
for (const row of rows){
|
|
133
|
+
let compression = 'none';
|
|
134
|
+
let storedBytes = 0;
|
|
135
|
+
try {
|
|
136
|
+
const manifest = JSON.parse(row.manifest_json);
|
|
137
|
+
const frames = Array.isArray(manifest.frames) ? manifest.frames : [];
|
|
138
|
+
let compressed = 0;
|
|
139
|
+
for (const frame of frames){
|
|
140
|
+
if (frame === null || typeof frame !== 'object') continue;
|
|
141
|
+
const record = frame;
|
|
142
|
+
if (record['compression'] === 'zstd') compressed += 1;
|
|
143
|
+
if (typeof record['bytes'] === 'number' && Number.isSafeInteger(record['bytes'])) storedBytes += record['bytes'];
|
|
144
|
+
}
|
|
145
|
+
compression = compressed === 0 ? 'none' : compressed === frames.length ? 'zstd' : 'mixed';
|
|
146
|
+
} catch {}
|
|
147
|
+
update.run(compression, storedBytes, DEFAULT_DATASET, row.session_id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function ensureSessionStatsColumns(handle) {
|
|
151
|
+
const columns = tableColumns(handle, 'sessions');
|
|
152
|
+
let added = false;
|
|
153
|
+
if (!columns.has('compression')) {
|
|
154
|
+
handle.exec("ALTER TABLE sessions ADD COLUMN compression TEXT NOT NULL DEFAULT 'none'");
|
|
155
|
+
added = true;
|
|
156
|
+
}
|
|
157
|
+
if (!columns.has('stored_bytes')) {
|
|
158
|
+
handle.exec('ALTER TABLE sessions ADD COLUMN stored_bytes INTEGER NOT NULL DEFAULT 0');
|
|
159
|
+
added = true;
|
|
160
|
+
}
|
|
161
|
+
if (added) backfillSessionStats(handle);
|
|
162
|
+
return added;
|
|
163
|
+
}
|
|
116
164
|
function migrateDatabase(handle, dataDir, version) {
|
|
117
165
|
if (version > SCHEMA_VERSION) {
|
|
118
166
|
throw new Error(`数据库结构版本 ${version} 高于服务端支持的 ${SCHEMA_VERSION};请升级服务端,不要降级覆盖。`);
|
|
119
167
|
}
|
|
120
168
|
if (version === SCHEMA_VERSION) {
|
|
121
169
|
handle.exec(SCHEMA);
|
|
170
|
+
const columns = tableColumns(handle, 'sessions');
|
|
171
|
+
if (!columns.has('compression') || !columns.has('stored_bytes')) {
|
|
172
|
+
backupBeforeMigration(handle, dataDir, version);
|
|
173
|
+
handle.exec('BEGIN IMMEDIATE');
|
|
174
|
+
try {
|
|
175
|
+
ensureSessionStatsColumns(handle);
|
|
176
|
+
handle.exec('COMMIT');
|
|
177
|
+
} catch (error) {
|
|
178
|
+
handle.exec('ROLLBACK');
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
122
182
|
return;
|
|
123
183
|
}
|
|
124
184
|
let sourceVersion = version;
|
|
@@ -150,6 +210,7 @@ function migrateDatabase(handle, dataDir, version) {
|
|
|
150
210
|
handle.exec('DROP TABLE IF EXISTS leases');
|
|
151
211
|
}
|
|
152
212
|
handle.exec(SCHEMA);
|
|
213
|
+
ensureSessionStatsColumns(handle);
|
|
153
214
|
handle.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
154
215
|
handle.exec('COMMIT');
|
|
155
216
|
} catch (error) {
|
|
@@ -225,6 +286,16 @@ export function readMaxBytes(db, fallback) {
|
|
|
225
286
|
export function writeMaxBytes(db, maxBytes) {
|
|
226
287
|
writeMeta(db, 'max_bytes', String(maxBytes));
|
|
227
288
|
}
|
|
289
|
+
export function readAppendRequest(db, deviceId, sessionId, key) {
|
|
290
|
+
const row = db.handle.prepare('SELECT request_hash, result_json FROM append_requests WHERE dataset = ? AND device_id = ? AND session_id = ? AND idempotency_key = ?').get(DEFAULT_DATASET, deviceId, sessionId, key);
|
|
291
|
+
return row === undefined ? undefined : {
|
|
292
|
+
requestHash: String(row.request_hash),
|
|
293
|
+
resultJson: String(row.result_json)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export function writeAppendRequest(db, deviceId, sessionId, key, requestHash, resultJson, createdAt) {
|
|
297
|
+
db.handle.prepare('INSERT INTO append_requests (dataset, device_id, session_id, idempotency_key, request_hash, result_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)').run(DEFAULT_DATASET, deviceId, sessionId, key, requestHash, resultJson, createdAt);
|
|
298
|
+
}
|
|
228
299
|
export function readRevision(db) {
|
|
229
300
|
return Number(readMeta(db, 'revision') ?? '0');
|
|
230
301
|
}
|