@artenx/dshb 0.1.31 → 0.1.32
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/lib/core/index.js +6 -1
- package/lib/core/index.js.map +1 -1
- package/lib/core/sync/coordinator.d.ts +2 -2
- package/lib/core/sync/coordinator.js +23 -6
- package/lib/core/sync/coordinator.js.map +1 -1
- package/lib/core/sync/internal-routes.d.ts +1 -1
- package/lib/core/sync/internal-routes.js +80 -4
- package/lib/core/sync/internal-routes.js.map +1 -1
- package/lib/core/sync/offline-archive.d.ts +1 -1
- package/lib/core/sync/offline-archive.js +1 -1
- package/lib/core/sync/offline-routes.js +1 -1
- package/lib/core/sync/protocol.d.ts +2 -2
- package/lib/core/sync/protocol.js +4 -1
- package/lib/core/sync/protocol.js.map +1 -1
- package/lib/core/sync/remote-helper.js +107 -76
- package/lib/core/sync/remote-helper.js.map +1 -1
- package/lib/core/sync/routes.d.ts +1 -1
- package/lib/core/sync/session-routes.d.ts +1 -1
- package/lib/core/sync/session-routes.js +1 -1
- package/lib/core/sync/session-sync.d.ts +2 -2
- package/lib/core/sync/session-sync.js +2 -2
- package/lib/core/sync/ssh-transport.d.ts +1 -1
- package/lib/core/sync/ssh-transport.js +100 -2
- package/lib/core/sync/ssh-transport.js.map +1 -1
- package/lib/{protocol-KaWZArY8.d.ts → protocol-DJ0zyVlB.d.ts} +5 -2
- package/lib/{session-sync-Zfo1Q-qT.d.ts → session-sync-BuOJvFxK.d.ts} +54 -2
- package/lib/{session-sync-BQt3HXkp.js → session-sync-CIqjuWkh.js} +455 -48
- package/lib/session-sync-CIqjuWkh.js.map +1 -0
- package/lib/{ssh-transport-Cfb_nwkY.d.ts → ssh-transport-B2oNDs0D.d.ts} +14 -4
- package/package.json +1 -1
- package/lib/session-sync-BQt3HXkp.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as SyncSessionError } from "../../session-sync-
|
|
1
|
+
import { n as SyncSessionError } from "../../session-sync-CIqjuWkh.js";
|
|
2
2
|
import { SyncOfflineArchiveError } from "./offline-archive.js";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY, SyncFrame, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
|
|
1
|
+
import { a as MAX_SYNC_TRANSFER_BYTES, c as SYNC_SESSION_EVENT_TAIL_CAPABILITY, d as SyncFrame, f as SyncFrameDecoder, h as encodeSyncDataFrame, i as MAX_SYNC_DATA_CHUNK_BYTES, l as SYNC_SESSION_MANIFEST_CAPABILITY, m as encodeSyncControlFrame, n as MAX_SESSION_OBJECT_STREAM_BYTES, o as SYNC_PROTOCOL_VERSION, p as SyncProtocolError, r as MAX_SYNC_CONTROL_FRAME_BYTES, s as SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, t as MAX_SESSION_OBJECT_INLINE_BYTES, u as SYNC_SESSION_OBJECT_STREAM_CAPABILITY } from "../../protocol-DJ0zyVlB.js";
|
|
2
|
+
export { MAX_SESSION_OBJECT_INLINE_BYTES, MAX_SESSION_OBJECT_STREAM_BYTES, MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY, SYNC_SESSION_OBJECT_STREAM_CAPABILITY, SyncFrame, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
|
|
@@ -4,9 +4,12 @@ const SYNC_PROTOCOL_VERSION = 1;
|
|
|
4
4
|
const SYNC_SESSION_MANIFEST_CAPABILITY = "session-manifest-v1";
|
|
5
5
|
const SYNC_SESSION_EVENT_TAIL_CAPABILITY = "session-event-tail-delta-v1";
|
|
6
6
|
const SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY = "session-bundle-objects-v1";
|
|
7
|
+
const SYNC_SESSION_OBJECT_STREAM_CAPABILITY = "session-object-stream-v1";
|
|
7
8
|
const MAX_SYNC_CONTROL_FRAME_BYTES = 65536;
|
|
8
9
|
const MAX_SYNC_DATA_CHUNK_BYTES = 1048576;
|
|
9
10
|
const MAX_SYNC_TRANSFER_BYTES = 134217728;
|
|
11
|
+
const MAX_SESSION_OBJECT_INLINE_BYTES = 262144;
|
|
12
|
+
const MAX_SESSION_OBJECT_STREAM_BYTES = 68719476736;
|
|
10
13
|
const FRAME_HEADER_BYTES = 5;
|
|
11
14
|
const CONTROL_FRAME = 0;
|
|
12
15
|
const DATA_FRAME = 1;
|
|
@@ -122,6 +125,6 @@ var SyncFrameDecoder = class {
|
|
|
122
125
|
}
|
|
123
126
|
};
|
|
124
127
|
//#endregion
|
|
125
|
-
export { MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
|
|
128
|
+
export { MAX_SESSION_OBJECT_INLINE_BYTES, MAX_SESSION_OBJECT_STREAM_BYTES, MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY, SYNC_SESSION_OBJECT_STREAM_CAPABILITY, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
|
|
126
129
|
|
|
127
130
|
//# sourceMappingURL=protocol.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","names":[],"sources":["../../../src/core/sync/protocol.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from 'node:crypto'\n\nexport const SYNC_PROTOCOL_VERSION = 1\nexport const SYNC_SESSION_MANIFEST_CAPABILITY = 'session-manifest-v1'\nexport const SYNC_SESSION_EVENT_TAIL_CAPABILITY = 'session-event-tail-delta-v1'\nexport const SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY = 'session-bundle-objects-v1'\nexport const MAX_SYNC_CONTROL_FRAME_BYTES = 64 * 1024\nexport const MAX_SYNC_DATA_CHUNK_BYTES = 1024 * 1024\n// 单次传输(bundle 请求/响应、原始附件/溢出流)的硬上限。必须远小于宿主进程\n// JS 堆(桌面端约 1.4GB):JSON.parse 的对象图膨胀约为 JSON 文本的 5-10 倍,\n// 100GB 级上限等于无内存保护。协调器按更小的目标分批传输(见 coordinator.ts)。\nexport const MAX_SYNC_TRANSFER_BYTES = 128 * 1024 * 1024\n\nconst FRAME_HEADER_BYTES = 5\nconst CONTROL_FRAME = 0\nconst DATA_FRAME = 1\nconst DATA_METADATA_BYTES = 36\n\nexport type SyncFrame =\n | { kind: 'control'; value: Record<string, unknown> }\n | { kind: 'data'; sequence: number; digest: string; data: Buffer }\n\nexport class SyncProtocolError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncProtocolError'\n }\n}\n\nfunction makeHeader(type: number, payloadLength: number): Buffer {\n const header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n header[0] = type\n header.writeUInt32BE(payloadLength, 1)\n return header\n}\n\nexport function encodeSyncControlFrame(value: Record<string, unknown>): Buffer {\n const payload = Buffer.from(`${JSON.stringify(value)}\\n`, 'utf8')\n if (payload.length > MAX_SYNC_CONTROL_FRAME_BYTES) throw new SyncProtocolError('control frame exceeds size limit')\n return Buffer.concat([makeHeader(CONTROL_FRAME, payload.length), payload])\n}\n\nexport function encodeSyncDataFrame(sequence: number, data: Buffer): Buffer {\n if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence > 0xffffffff) {\n throw new SyncProtocolError('data sequence is invalid')\n }\n if (data.length > MAX_SYNC_DATA_CHUNK_BYTES) throw new SyncProtocolError('data chunk exceeds size limit')\n const metadata = Buffer.allocUnsafe(DATA_METADATA_BYTES)\n metadata.writeUInt32BE(sequence, 0)\n createHash('sha256').update(data).digest().copy(metadata, 4)\n return Buffer.concat([makeHeader(DATA_FRAME, metadata.length + data.length), metadata, data])\n}\n\nexport class SyncFrameDecoder {\n private readonly header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n private headerOffset = 0\n private frameType = -1\n private payload: Buffer | undefined\n private payloadOffset = 0\n private nextDataSequence = 0\n private ended = false\n\n push(chunk: Buffer): SyncFrame[] {\n if (this.ended) throw new SyncProtocolError('frame decoder is already finished')\n const frames: SyncFrame[] = []\n let inputOffset = 0\n\n while (inputOffset < chunk.length) {\n if (!this.payload) {\n const copied = Math.min(FRAME_HEADER_BYTES - this.headerOffset, chunk.length - inputOffset)\n chunk.copy(this.header, this.headerOffset, inputOffset, inputOffset + copied)\n this.headerOffset += copied\n inputOffset += copied\n if (this.headerOffset < FRAME_HEADER_BYTES) continue\n\n this.frameType = this.header[0]\n const payloadLength = this.header.readUInt32BE(1)\n if (this.frameType === CONTROL_FRAME) {\n if (payloadLength < 2 || payloadLength > MAX_SYNC_CONTROL_FRAME_BYTES) {\n throw new SyncProtocolError('control frame length is invalid')\n }\n } else if (this.frameType === DATA_FRAME) {\n if (payloadLength < DATA_METADATA_BYTES || payloadLength > DATA_METADATA_BYTES + MAX_SYNC_DATA_CHUNK_BYTES) {\n throw new SyncProtocolError('data frame length is invalid')\n }\n } else {\n throw new SyncProtocolError('unknown frame type')\n }\n this.payload = Buffer.allocUnsafe(payloadLength)\n this.payloadOffset = 0\n }\n\n const payload = this.payload\n const copied = Math.min(payload.length - this.payloadOffset, chunk.length - inputOffset)\n chunk.copy(payload, this.payloadOffset, inputOffset, inputOffset + copied)\n this.payloadOffset += copied\n inputOffset += copied\n\n if (this.payloadOffset === payload.length) {\n frames.push(this.decodeFrame(this.frameType, payload))\n this.resetFrame()\n }\n }\n return frames\n }\n\n finish(): void {\n if (this.headerOffset !== 0 || this.payload !== undefined) throw new SyncProtocolError('truncated frame stream')\n this.ended = true\n }\n\n private decodeFrame(type: number, payload: Buffer): SyncFrame {\n if (type === CONTROL_FRAME) {\n if (payload[payload.length - 1] !== 0x0a) throw new SyncProtocolError('control frame must end with a newline')\n let value: unknown\n try {\n value = JSON.parse(payload.subarray(0, -1).toString('utf8'))\n } catch {\n throw new SyncProtocolError('control frame contains invalid JSON')\n }\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncProtocolError('control frame must contain an object')\n }\n return { kind: 'control', value: value as Record<string, unknown> }\n }\n\n const sequence = payload.readUInt32BE(0)\n if (sequence !== this.nextDataSequence) throw new SyncProtocolError('data sequence is out of order')\n const expectedDigest = payload.subarray(4, DATA_METADATA_BYTES)\n const data = payload.subarray(DATA_METADATA_BYTES)\n const actualDigest = createHash('sha256').update(data).digest()\n if (!timingSafeEqual(expectedDigest, actualDigest)) throw new SyncProtocolError('data chunk digest mismatch')\n this.nextDataSequence += 1\n return { kind: 'data', sequence, digest: actualDigest.toString('hex'), data: Buffer.from(data) }\n }\n\n private resetFrame(): void {\n this.headerOffset = 0\n this.frameType = -1\n this.payload = undefined\n this.payloadOffset = 0\n }\n}\n"],"mappings":";;AAEA,MAAa,wBAAwB;AACrC,MAAa,mCAAmC;AAChD,MAAa,qCAAqC;AAClD,MAAa,yCAAyC;
|
|
1
|
+
{"version":3,"file":"protocol.js","names":[],"sources":["../../../src/core/sync/protocol.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from 'node:crypto'\n\nexport const SYNC_PROTOCOL_VERSION = 1\nexport const SYNC_SESSION_MANIFEST_CAPABILITY = 'session-manifest-v1'\nexport const SYNC_SESSION_EVENT_TAIL_CAPABILITY = 'session-event-tail-delta-v1'\nexport const SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY = 'session-bundle-objects-v1'\n// 对象带外流:附件/spill 负载不再内联进 bundle JSON,而是通过专用 RPC 以\n// 磁盘到磁盘的帧流传输(内存占用 O(单帧)),单对象与会话引用总量不再受\n// JSON 传输上限约束。两端都声明该能力时才启用,旧版本对端回退内联路径。\nexport const SYNC_SESSION_OBJECT_STREAM_CAPABILITY = 'session-object-stream-v1'\nexport const MAX_SYNC_CONTROL_FRAME_BYTES = 64 * 1024\nexport const MAX_SYNC_DATA_CHUNK_BYTES = 1024 * 1024\n// 单次传输(bundle 请求/响应、原始附件/溢出流)的硬上限。必须远小于宿主进程\n// JS 堆(桌面端约 1.4GB):JSON.parse 的对象图膨胀约为 JSON 文本的 5-10 倍,\n// 100GB 级上限等于无内存保护。协调器按更小的目标分批传输(见 coordinator.ts)。\n// 注意:该上限只约束 JSON bundle;对象带外流是磁盘流式传输,走独立上限。\nexport const MAX_SYNC_TRANSFER_BYTES = 128 * 1024 * 1024\n// 对象内联进 bundle JSON 的字节阈值:小对象内联(减少往返),大对象走带外流。\nexport const MAX_SESSION_OBJECT_INLINE_BYTES = 256 * 1024\n// 单个带外对象流的字节上限:磁盘到磁盘传输,内存不随体积增长,上限只为\n// 防失控对端无限发送。64GB 覆盖一切真实会话负载。\nexport const MAX_SESSION_OBJECT_STREAM_BYTES = 64 * 1024 * 1024 * 1024\n\nconst FRAME_HEADER_BYTES = 5\nconst CONTROL_FRAME = 0\nconst DATA_FRAME = 1\nconst DATA_METADATA_BYTES = 36\n\nexport type SyncFrame =\n | { kind: 'control'; value: Record<string, unknown> }\n | { kind: 'data'; sequence: number; digest: string; data: Buffer }\n\nexport class SyncProtocolError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncProtocolError'\n }\n}\n\nfunction makeHeader(type: number, payloadLength: number): Buffer {\n const header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n header[0] = type\n header.writeUInt32BE(payloadLength, 1)\n return header\n}\n\nexport function encodeSyncControlFrame(value: Record<string, unknown>): Buffer {\n const payload = Buffer.from(`${JSON.stringify(value)}\\n`, 'utf8')\n if (payload.length > MAX_SYNC_CONTROL_FRAME_BYTES) throw new SyncProtocolError('control frame exceeds size limit')\n return Buffer.concat([makeHeader(CONTROL_FRAME, payload.length), payload])\n}\n\nexport function encodeSyncDataFrame(sequence: number, data: Buffer): Buffer {\n if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence > 0xffffffff) {\n throw new SyncProtocolError('data sequence is invalid')\n }\n if (data.length > MAX_SYNC_DATA_CHUNK_BYTES) throw new SyncProtocolError('data chunk exceeds size limit')\n const metadata = Buffer.allocUnsafe(DATA_METADATA_BYTES)\n metadata.writeUInt32BE(sequence, 0)\n createHash('sha256').update(data).digest().copy(metadata, 4)\n return Buffer.concat([makeHeader(DATA_FRAME, metadata.length + data.length), metadata, data])\n}\n\nexport class SyncFrameDecoder {\n private readonly header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n private headerOffset = 0\n private frameType = -1\n private payload: Buffer | undefined\n private payloadOffset = 0\n private nextDataSequence = 0\n private ended = false\n\n push(chunk: Buffer): SyncFrame[] {\n if (this.ended) throw new SyncProtocolError('frame decoder is already finished')\n const frames: SyncFrame[] = []\n let inputOffset = 0\n\n while (inputOffset < chunk.length) {\n if (!this.payload) {\n const copied = Math.min(FRAME_HEADER_BYTES - this.headerOffset, chunk.length - inputOffset)\n chunk.copy(this.header, this.headerOffset, inputOffset, inputOffset + copied)\n this.headerOffset += copied\n inputOffset += copied\n if (this.headerOffset < FRAME_HEADER_BYTES) continue\n\n this.frameType = this.header[0]\n const payloadLength = this.header.readUInt32BE(1)\n if (this.frameType === CONTROL_FRAME) {\n if (payloadLength < 2 || payloadLength > MAX_SYNC_CONTROL_FRAME_BYTES) {\n throw new SyncProtocolError('control frame length is invalid')\n }\n } else if (this.frameType === DATA_FRAME) {\n if (payloadLength < DATA_METADATA_BYTES || payloadLength > DATA_METADATA_BYTES + MAX_SYNC_DATA_CHUNK_BYTES) {\n throw new SyncProtocolError('data frame length is invalid')\n }\n } else {\n throw new SyncProtocolError('unknown frame type')\n }\n this.payload = Buffer.allocUnsafe(payloadLength)\n this.payloadOffset = 0\n }\n\n const payload = this.payload\n const copied = Math.min(payload.length - this.payloadOffset, chunk.length - inputOffset)\n chunk.copy(payload, this.payloadOffset, inputOffset, inputOffset + copied)\n this.payloadOffset += copied\n inputOffset += copied\n\n if (this.payloadOffset === payload.length) {\n frames.push(this.decodeFrame(this.frameType, payload))\n this.resetFrame()\n }\n }\n return frames\n }\n\n finish(): void {\n if (this.headerOffset !== 0 || this.payload !== undefined) throw new SyncProtocolError('truncated frame stream')\n this.ended = true\n }\n\n private decodeFrame(type: number, payload: Buffer): SyncFrame {\n if (type === CONTROL_FRAME) {\n if (payload[payload.length - 1] !== 0x0a) throw new SyncProtocolError('control frame must end with a newline')\n let value: unknown\n try {\n value = JSON.parse(payload.subarray(0, -1).toString('utf8'))\n } catch {\n throw new SyncProtocolError('control frame contains invalid JSON')\n }\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncProtocolError('control frame must contain an object')\n }\n return { kind: 'control', value: value as Record<string, unknown> }\n }\n\n const sequence = payload.readUInt32BE(0)\n if (sequence !== this.nextDataSequence) throw new SyncProtocolError('data sequence is out of order')\n const expectedDigest = payload.subarray(4, DATA_METADATA_BYTES)\n const data = payload.subarray(DATA_METADATA_BYTES)\n const actualDigest = createHash('sha256').update(data).digest()\n if (!timingSafeEqual(expectedDigest, actualDigest)) throw new SyncProtocolError('data chunk digest mismatch')\n this.nextDataSequence += 1\n return { kind: 'data', sequence, digest: actualDigest.toString('hex'), data: Buffer.from(data) }\n }\n\n private resetFrame(): void {\n this.headerOffset = 0\n this.frameType = -1\n this.payload = undefined\n this.payloadOffset = 0\n }\n}\n"],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ const { createHash, timingSafeEqual } = require('node:crypto')
|
|
|
5
5
|
const MAX_CONTROL = 65536
|
|
6
6
|
const MAX_CHUNK = 1048576
|
|
7
7
|
const MAX_TRANSFER = 107374182400
|
|
8
|
+
const OBJECT_STREAM_LIMIT = 68719476736
|
|
8
9
|
const PATHS = {
|
|
9
10
|
handshake: { path: '/api/dshb/sync/internal/handshake', method: 'GET' },
|
|
10
11
|
'node-manifest': { path: '/api/dshb/sync/internal/nodes/manifest', method: 'GET' },
|
|
@@ -13,9 +14,11 @@ const PATHS = {
|
|
|
13
14
|
'session-manifest': { path: '/api/dshb/sync/internal/sessions/manifest', method: 'GET' },
|
|
14
15
|
'session-entries': { path: '/api/dshb/sync/internal/sessions/entries', method: 'POST' },
|
|
15
16
|
'session-apply': { path: '/api/dshb/sync/internal/sessions/apply', method: 'POST' },
|
|
17
|
+
'session-objects': { path: '/api/dshb/sync/internal/sessions/objects', method: 'POST' },
|
|
18
|
+
'session-objects-push': { path: '/api/dshb/sync/internal/sessions/objects', method: 'POST' },
|
|
16
19
|
}
|
|
17
|
-
const
|
|
18
|
-
|
|
20
|
+
const BUFFERED_BODY_OPERATIONS = ['node-apply', 'session-entries', 'session-apply', 'session-objects']
|
|
21
|
+
const STREAMING_RESPONSE_OPERATIONS = ['node-manifest', 'session-bundle', 'session-manifest', 'session-entries', 'session-objects']
|
|
19
22
|
|
|
20
23
|
function encodeControl(value) {
|
|
21
24
|
const body = Buffer.from(JSON.stringify(value) + '\n', 'utf8')
|
|
@@ -38,69 +41,116 @@ function encodeData(sequence, data) {
|
|
|
38
41
|
return Buffer.concat([header, payload])
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
frames.push({ kind: 'data', data })
|
|
44
|
+
function decodeOneFrame(kind, payload, state) {
|
|
45
|
+
if (kind === 0) {
|
|
46
|
+
if (payload.length < 2 || payload[payload.length - 1] !== 10) throw new Error('invalid control frame')
|
|
47
|
+
return { kind: 'control', value: JSON.parse(payload.subarray(0, -1).toString('utf8')) }
|
|
48
|
+
}
|
|
49
|
+
if (payload.length < 36) throw new Error('invalid data frame')
|
|
50
|
+
const currentSequence = payload.readUInt32BE(0)
|
|
51
|
+
const expected = payload.subarray(4, 36)
|
|
52
|
+
const data = payload.subarray(36)
|
|
53
|
+
const actual = createHash('sha256').update(data).digest()
|
|
54
|
+
if (currentSequence !== state.sequence || !timingSafeEqual(expected, actual)) throw new Error('invalid data chunk')
|
|
55
|
+
state.sequence += 1
|
|
56
|
+
state.totalData += data.length
|
|
57
|
+
if (state.totalData > MAX_TRANSFER && state.totalData > OBJECT_STREAM_LIMIT) throw new Error('transfer exceeds size limit')
|
|
58
|
+
return { kind: 'data', data }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readFrame(iterator, state) {
|
|
62
|
+
for (;;) {
|
|
63
|
+
if (state.buffer.length >= 5) {
|
|
64
|
+
const kind = state.buffer[0]
|
|
65
|
+
const length = state.buffer.readUInt32BE(1)
|
|
66
|
+
const maxLength = kind === 0 ? MAX_CONTROL : 36 + MAX_CHUNK
|
|
67
|
+
if ((kind !== 0 && kind !== 1) || length > maxLength) throw new Error('invalid frame length')
|
|
68
|
+
if (state.buffer.length >= 5 + length) {
|
|
69
|
+
const payload = state.buffer.subarray(5, 5 + length)
|
|
70
|
+
state.buffer = state.buffer.subarray(5 + length)
|
|
71
|
+
return decodeOneFrame(kind, payload, state)
|
|
72
|
+
}
|
|
71
73
|
}
|
|
74
|
+
const next = await iterator.next()
|
|
75
|
+
if (next.done) throw new Error('helper input ended inside a frame')
|
|
76
|
+
state.buffer = Buffer.concat([state.buffer, Buffer.from(next.value)])
|
|
72
77
|
}
|
|
73
|
-
return frames
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
function
|
|
77
|
-
const frames = decodeFrames(buffer)
|
|
78
|
-
const first = frames[0]
|
|
79
|
-
if (!first || first.kind !== 'control') throw new Error('request control frame required')
|
|
80
|
-
const request = first.value
|
|
80
|
+
function validateRequest(request) {
|
|
81
81
|
if (!request || request.kind !== 'request' || request.protocolVersion !== 1 || !Object.prototype.hasOwnProperty.call(PATHS, request.operation)) {
|
|
82
82
|
throw new Error('unsupported helper request')
|
|
83
83
|
}
|
|
84
|
-
if (Object.keys(request).some((key)
|
|
84
|
+
if (Object.keys(request).some(function (key) { return ['kind', 'protocolVersion', 'operation', 'port'].indexOf(key) < 0 })) {
|
|
85
85
|
throw new Error('unsupported helper request field')
|
|
86
86
|
}
|
|
87
87
|
if (!Number.isInteger(request.port) || request.port < 1 || request.port > 65535) throw new Error('invalid loopback port')
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
if (frames.length !== 1) throw new Error('unexpected helper request frames')
|
|
97
|
-
return { request, operation }
|
|
88
|
+
return PATHS[request.operation]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeStdout(data) {
|
|
92
|
+
return new Promise(function (resolve, reject) {
|
|
93
|
+
if (process.stdout.write(data)) resolve()
|
|
94
|
+
else process.stdout.once('drain', resolve)
|
|
95
|
+
})
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
async function run() {
|
|
101
|
-
const
|
|
102
|
-
const {
|
|
99
|
+
const iterator = process.stdin[Symbol.asyncIterator]()
|
|
100
|
+
const state = { buffer: Buffer.alloc(0), sequence: 0, totalData: 0 }
|
|
101
|
+
const first = await readFrame(iterator, state)
|
|
102
|
+
if (first.kind !== 'control' || first.value.kind !== 'request') throw new Error('request control frame required')
|
|
103
|
+
const request = first.value
|
|
104
|
+
const operation = validateRequest(request)
|
|
103
105
|
const url = 'http://127.0.0.1:' + request.port + operation.path
|
|
106
|
+
|
|
107
|
+
if (request.operation === 'session-objects-push') {
|
|
108
|
+
// 对象推送:请求体(对象帧流)直接转发为 HTTP 流式请求体,内存 O(帧)。
|
|
109
|
+
const { Readable } = require('node:stream')
|
|
110
|
+
const bodyStream = new Readable({ read: function () {} })
|
|
111
|
+
const pump = (async function () {
|
|
112
|
+
try {
|
|
113
|
+
for (;;) {
|
|
114
|
+
const frame = await readFrame(iterator, state)
|
|
115
|
+
if (frame.kind === 'control' && frame.value.kind === 'request-end') break
|
|
116
|
+
if (frame.kind !== 'data') throw new Error('invalid object push frame')
|
|
117
|
+
if (!bodyStream.push(Buffer.from(frame.data))) {
|
|
118
|
+
await new Promise(function (resolve) { bodyStream.once('drain', resolve) })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
bodyStream.push(null)
|
|
122
|
+
} catch (error) {
|
|
123
|
+
bodyStream.destroy(error)
|
|
124
|
+
}
|
|
125
|
+
})()
|
|
126
|
+
const response = await fetch(url, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
redirect: 'error',
|
|
129
|
+
headers: { 'content-type': 'application/x-dshb-object-stream' },
|
|
130
|
+
body: bodyStream,
|
|
131
|
+
duplex: 'half',
|
|
132
|
+
})
|
|
133
|
+
const text = await response.text()
|
|
134
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_CONTROL) throw new Error('response control frame too large')
|
|
135
|
+
await writeStdout(encodeControl({ kind: 'response', status: response.status, body: JSON.parse(text) }))
|
|
136
|
+
if (!response.ok) process.exitCode = 1
|
|
137
|
+
await pump
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let body
|
|
142
|
+
if (BUFFERED_BODY_OPERATIONS.indexOf(request.operation) >= 0) {
|
|
143
|
+
// 帧序:control(request) → data… → control(request-end)。
|
|
144
|
+
const chunks = []
|
|
145
|
+
for (;;) {
|
|
146
|
+
const frame = await readFrame(iterator, state)
|
|
147
|
+
if (frame.kind === 'control' && frame.value.kind === 'request-end') break
|
|
148
|
+
if (frame.kind !== 'data') throw new Error('invalid apply request body')
|
|
149
|
+
chunks.push(frame.data)
|
|
150
|
+
}
|
|
151
|
+
body = Buffer.concat(chunks)
|
|
152
|
+
}
|
|
153
|
+
|
|
104
154
|
const response = await fetch(url, {
|
|
105
155
|
method: operation.method,
|
|
106
156
|
redirect: 'error',
|
|
@@ -108,23 +158,15 @@ async function run() {
|
|
|
108
158
|
...(body ? { body } : {}),
|
|
109
159
|
})
|
|
110
160
|
|
|
111
|
-
|
|
112
|
-
return new Promise(function(resolve, reject) {
|
|
113
|
-
if (process.stdout.write(data)) resolve()
|
|
114
|
-
else process.stdout.once('drain', resolve)
|
|
115
|
-
})
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (request.operation === 'node-manifest' || request.operation === 'session-bundle' ||
|
|
119
|
-
request.operation === 'session-manifest' || request.operation === 'session-entries') {
|
|
161
|
+
if (STREAMING_RESPONSE_OPERATIONS.indexOf(request.operation) >= 0) {
|
|
120
162
|
await writeStdout(encodeControl({ kind: 'response', status: response.status, bodyMode: 'data' }))
|
|
121
|
-
if (!response.body) throw new Error('
|
|
163
|
+
if (!response.body) throw new Error('streaming response body missing')
|
|
122
164
|
var sequence = 0
|
|
123
165
|
var total = 0
|
|
124
166
|
for await (var chunk of response.body) {
|
|
125
167
|
var data = Buffer.from(chunk)
|
|
126
168
|
total += data.length
|
|
127
|
-
if (total > MAX_TRANSFER) throw new Error('
|
|
169
|
+
if (total > MAX_TRANSFER && total > OBJECT_STREAM_LIMIT) throw new Error('response exceeds size limit')
|
|
128
170
|
for (var offset = 0; offset < data.length; offset += MAX_CHUNK) {
|
|
129
171
|
await writeStdout(encodeData(sequence++, data.subarray(offset, Math.min(offset + MAX_CHUNK, data.length))))
|
|
130
172
|
}
|
|
@@ -143,20 +185,9 @@ async function run() {
|
|
|
143
185
|
if (!response.ok && !recoverableDeltaMismatch) process.exitCode = 1
|
|
144
186
|
}
|
|
145
187
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
process.exitCode = 1
|
|
150
|
-
process.stdin.destroy()
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
input.push(Buffer.from(chunk))
|
|
154
|
-
})
|
|
155
|
-
process.stdin.on('end', () => {
|
|
156
|
-
run().catch(() => {
|
|
157
|
-
try { process.stdout.write(encodeControl({ kind: 'error', code: 'helper-failed' })) } catch {}
|
|
158
|
-
process.exitCode = 1
|
|
159
|
-
})
|
|
188
|
+
run().catch(function () {
|
|
189
|
+
try { process.stdout.write(encodeControl({ kind: 'error', code: 'helper-failed' })) } catch (err) {}
|
|
190
|
+
process.exitCode = 1
|
|
160
191
|
})
|
|
161
192
|
`;
|
|
162
193
|
const REMOTE_SYNC_HELPER_COMMAND = `node -e 'eval(Buffer.from("${Buffer.from(REMOTE_HELPER_SOURCE, "utf8").toString("base64")}","base64").toString("utf8"))'`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-helper.js","names":[],"sources":["../../../src/core/sync/remote-helper.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nconst REMOTE_HELPER_SOURCE = String.raw`'use strict'\nconst { createHash, timingSafeEqual } = require('node:crypto')\nconst MAX_CONTROL = 65536\nconst MAX_CHUNK = 1048576\nconst MAX_TRANSFER = 107374182400\nconst PATHS = {\n handshake: { path: '/api/dshb/sync/internal/handshake', method: 'GET' },\n 'node-manifest': { path: '/api/dshb/sync/internal/nodes/manifest', method: 'GET' },\n 'node-apply': { path: '/api/dshb/sync/internal/nodes/apply', method: 'POST' },\n 'session-bundle': { path: '/api/dshb/sync/internal/sessions/bundle', method: 'GET' },\n 'session-manifest': { path: '/api/dshb/sync/internal/sessions/manifest', method: 'GET' },\n 'session-entries': { path: '/api/dshb/sync/internal/sessions/entries', method: 'POST' },\n 'session-apply': { path: '/api/dshb/sync/internal/sessions/apply', method: 'POST' },\n}\nconst
|
|
1
|
+
{"version":3,"file":"remote-helper.js","names":[],"sources":["../../../src/core/sync/remote-helper.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nconst REMOTE_HELPER_SOURCE = String.raw`'use strict'\nconst { createHash, timingSafeEqual } = require('node:crypto')\nconst MAX_CONTROL = 65536\nconst MAX_CHUNK = 1048576\nconst MAX_TRANSFER = 107374182400\nconst OBJECT_STREAM_LIMIT = 68719476736\nconst PATHS = {\n handshake: { path: '/api/dshb/sync/internal/handshake', method: 'GET' },\n 'node-manifest': { path: '/api/dshb/sync/internal/nodes/manifest', method: 'GET' },\n 'node-apply': { path: '/api/dshb/sync/internal/nodes/apply', method: 'POST' },\n 'session-bundle': { path: '/api/dshb/sync/internal/sessions/bundle', method: 'GET' },\n 'session-manifest': { path: '/api/dshb/sync/internal/sessions/manifest', method: 'GET' },\n 'session-entries': { path: '/api/dshb/sync/internal/sessions/entries', method: 'POST' },\n 'session-apply': { path: '/api/dshb/sync/internal/sessions/apply', method: 'POST' },\n 'session-objects': { path: '/api/dshb/sync/internal/sessions/objects', method: 'POST' },\n 'session-objects-push': { path: '/api/dshb/sync/internal/sessions/objects', method: 'POST' },\n}\nconst BUFFERED_BODY_OPERATIONS = ['node-apply', 'session-entries', 'session-apply', 'session-objects']\nconst STREAMING_RESPONSE_OPERATIONS = ['node-manifest', 'session-bundle', 'session-manifest', 'session-entries', 'session-objects']\n\nfunction encodeControl(value) {\n const body = Buffer.from(JSON.stringify(value) + '\\n', 'utf8')\n if (body.length > MAX_CONTROL) throw new Error('control frame too large')\n const header = Buffer.alloc(5)\n header[0] = 0\n header.writeUInt32BE(body.length, 1)\n return Buffer.concat([header, body])\n}\n\nfunction encodeData(sequence, data) {\n const digest = createHash('sha256').update(data).digest()\n const metadata = Buffer.alloc(36)\n metadata.writeUInt32BE(sequence, 0)\n digest.copy(metadata, 4)\n const payload = Buffer.concat([metadata, data])\n const header = Buffer.alloc(5)\n header[0] = 1\n header.writeUInt32BE(payload.length, 1)\n return Buffer.concat([header, payload])\n}\n\nfunction decodeOneFrame(kind, payload, state) {\n if (kind === 0) {\n if (payload.length < 2 || payload[payload.length - 1] !== 10) throw new Error('invalid control frame')\n return { kind: 'control', value: JSON.parse(payload.subarray(0, -1).toString('utf8')) }\n }\n if (payload.length < 36) throw new Error('invalid data frame')\n const currentSequence = payload.readUInt32BE(0)\n const expected = payload.subarray(4, 36)\n const data = payload.subarray(36)\n const actual = createHash('sha256').update(data).digest()\n if (currentSequence !== state.sequence || !timingSafeEqual(expected, actual)) throw new Error('invalid data chunk')\n state.sequence += 1\n state.totalData += data.length\n if (state.totalData > MAX_TRANSFER && state.totalData > OBJECT_STREAM_LIMIT) throw new Error('transfer exceeds size limit')\n return { kind: 'data', data }\n}\n\nasync function readFrame(iterator, state) {\n for (;;) {\n if (state.buffer.length >= 5) {\n const kind = state.buffer[0]\n const length = state.buffer.readUInt32BE(1)\n const maxLength = kind === 0 ? MAX_CONTROL : 36 + MAX_CHUNK\n if ((kind !== 0 && kind !== 1) || length > maxLength) throw new Error('invalid frame length')\n if (state.buffer.length >= 5 + length) {\n const payload = state.buffer.subarray(5, 5 + length)\n state.buffer = state.buffer.subarray(5 + length)\n return decodeOneFrame(kind, payload, state)\n }\n }\n const next = await iterator.next()\n if (next.done) throw new Error('helper input ended inside a frame')\n state.buffer = Buffer.concat([state.buffer, Buffer.from(next.value)])\n }\n}\n\nfunction validateRequest(request) {\n if (!request || request.kind !== 'request' || request.protocolVersion !== 1 || !Object.prototype.hasOwnProperty.call(PATHS, request.operation)) {\n throw new Error('unsupported helper request')\n }\n if (Object.keys(request).some(function (key) { return ['kind', 'protocolVersion', 'operation', 'port'].indexOf(key) < 0 })) {\n throw new Error('unsupported helper request field')\n }\n if (!Number.isInteger(request.port) || request.port < 1 || request.port > 65535) throw new Error('invalid loopback port')\n return PATHS[request.operation]\n}\n\nfunction writeStdout(data) {\n return new Promise(function (resolve, reject) {\n if (process.stdout.write(data)) resolve()\n else process.stdout.once('drain', resolve)\n })\n}\n\nasync function run() {\n const iterator = process.stdin[Symbol.asyncIterator]()\n const state = { buffer: Buffer.alloc(0), sequence: 0, totalData: 0 }\n const first = await readFrame(iterator, state)\n if (first.kind !== 'control' || first.value.kind !== 'request') throw new Error('request control frame required')\n const request = first.value\n const operation = validateRequest(request)\n const url = 'http://127.0.0.1:' + request.port + operation.path\n\n if (request.operation === 'session-objects-push') {\n // 对象推送:请求体(对象帧流)直接转发为 HTTP 流式请求体,内存 O(帧)。\n const { Readable } = require('node:stream')\n const bodyStream = new Readable({ read: function () {} })\n const pump = (async function () {\n try {\n for (;;) {\n const frame = await readFrame(iterator, state)\n if (frame.kind === 'control' && frame.value.kind === 'request-end') break\n if (frame.kind !== 'data') throw new Error('invalid object push frame')\n if (!bodyStream.push(Buffer.from(frame.data))) {\n await new Promise(function (resolve) { bodyStream.once('drain', resolve) })\n }\n }\n bodyStream.push(null)\n } catch (error) {\n bodyStream.destroy(error)\n }\n })()\n const response = await fetch(url, {\n method: 'POST',\n redirect: 'error',\n headers: { 'content-type': 'application/x-dshb-object-stream' },\n body: bodyStream,\n duplex: 'half',\n })\n const text = await response.text()\n if (Buffer.byteLength(text, 'utf8') > MAX_CONTROL) throw new Error('response control frame too large')\n await writeStdout(encodeControl({ kind: 'response', status: response.status, body: JSON.parse(text) }))\n if (!response.ok) process.exitCode = 1\n await pump\n return\n }\n\n let body\n if (BUFFERED_BODY_OPERATIONS.indexOf(request.operation) >= 0) {\n // 帧序:control(request) → data… → control(request-end)。\n const chunks = []\n for (;;) {\n const frame = await readFrame(iterator, state)\n if (frame.kind === 'control' && frame.value.kind === 'request-end') break\n if (frame.kind !== 'data') throw new Error('invalid apply request body')\n chunks.push(frame.data)\n }\n body = Buffer.concat(chunks)\n }\n\n const response = await fetch(url, {\n method: operation.method,\n redirect: 'error',\n headers: { accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) },\n ...(body ? { body } : {}),\n })\n\n if (STREAMING_RESPONSE_OPERATIONS.indexOf(request.operation) >= 0) {\n await writeStdout(encodeControl({ kind: 'response', status: response.status, bodyMode: 'data' }))\n if (!response.body) throw new Error('streaming response body missing')\n var sequence = 0\n var total = 0\n for await (var chunk of response.body) {\n var data = Buffer.from(chunk)\n total += data.length\n if (total > MAX_TRANSFER && total > OBJECT_STREAM_LIMIT) throw new Error('response exceeds size limit')\n for (var offset = 0; offset < data.length; offset += MAX_CHUNK) {\n await writeStdout(encodeData(sequence++, data.subarray(offset, Math.min(offset + MAX_CHUNK, data.length))))\n }\n }\n await writeStdout(encodeControl({ kind: 'response-end' }))\n if (!response.ok) process.exitCode = 1\n return\n }\n\n var text = await response.text()\n if (Buffer.byteLength(text, 'utf8') > MAX_CONTROL) throw new Error('response control frame too large')\n var responseBody = JSON.parse(text)\n await writeStdout(encodeControl({ kind: 'response', status: response.status, body: responseBody }))\n var recoverableDeltaMismatch = request.operation === 'session-apply' && response.status === 409 &&\n responseBody && responseBody.code === 'session-delta-prefix'\n if (!response.ok && !recoverableDeltaMismatch) process.exitCode = 1\n}\n\nrun().catch(function () {\n try { process.stdout.write(encodeControl({ kind: 'error', code: 'helper-failed' })) } catch (err) {}\n process.exitCode = 1\n})\n`\n\nconst REMOTE_HELPER_BASE64 = Buffer.from(REMOTE_HELPER_SOURCE, 'utf8').toString('base64')\n\nexport const REMOTE_SYNC_HELPER_COMMAND =\n `node -e 'eval(Buffer.from(\"${REMOTE_HELPER_BASE64}\",\"base64\").toString(\"utf8\"))'`\n"],"mappings":";;AAEA,MAAM,uBAAuB,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiMvC,MAAa,6BACX,8BAH2B,OAAO,KAAK,sBAAsB,MAAM,CAAC,CAAC,SAAS,QAGhD,EAAqB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyncPeerManager } from "./peer-manager.js";
|
|
2
|
-
import { s as SyncSshTransport } from "../../ssh-transport-
|
|
2
|
+
import { s as SyncSshTransport } from "../../ssh-transport-B2oNDs0D.js";
|
|
3
3
|
import { SyncCoordinator } from "./coordinator.js";
|
|
4
4
|
import { Context } from "@deepseek-ai/cordis";
|
|
5
5
|
//#region src/core/sync/routes.d.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as SyncSessionService } from "../../session-sync-BuOJvFxK.js";
|
|
2
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
3
|
//#region src/core/sync/session-routes.d.ts
|
|
4
4
|
declare function registerSyncSessionRoutes(ctx: Context, sessions: SyncSessionService): void;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as validateSyncSessionBundle, S as
|
|
2
|
-
export { ExportObjectCache, SyncSessionApplyOptions, SyncSessionApplyResult, SyncSessionBundle, SyncSessionEntry, SyncSessionError, SyncSessionEventDelta, SyncSessionEventPrefix, SyncSessionInstructionSnapshot, SyncSessionManifest, SyncSessionManifestEntry, SyncSessionObject, SyncSessionService, SyncSessionTransferPlan, SyncSessionWorkspace, WorkspaceRegistryLike, estimateJsonBytes, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateSyncSessionBundle, validateSyncSessionManifest };
|
|
1
|
+
import { C as sessionEventListDigest, D as validateSyncSessionManifest, E as validateSyncSessionBundle, S as planSessionTransfers, T as validateObjectPayload, _ as SyncSessionWorkspace, a as SyncSessionBundle, b as makeSyncSessionBundle, c as SyncSessionEventDelta, d as SyncSessionManifest, f as SyncSessionManifestEntry, g as SyncSessionTransferPlan, h as SyncSessionService, i as SyncSessionApplyResult, l as SyncSessionEventPrefix, m as SyncSessionObjectInbox, n as SyncObjectInboxWriter, o as SyncSessionEntry, p as SyncSessionObject, r as SyncSessionApplyOptions, s as SyncSessionError, t as ExportObjectCache, u as SyncSessionInstructionSnapshot, v as WorkspaceRegistryLike, w as syncSessionEntryKey, x as makeSyncSessionManifest, y as estimateJsonBytes } from "../../session-sync-BuOJvFxK.js";
|
|
2
|
+
export { ExportObjectCache, SyncObjectInboxWriter, SyncSessionApplyOptions, SyncSessionApplyResult, SyncSessionBundle, SyncSessionEntry, SyncSessionError, SyncSessionEventDelta, SyncSessionEventPrefix, SyncSessionInstructionSnapshot, SyncSessionManifest, SyncSessionManifestEntry, SyncSessionObject, SyncSessionObjectInbox, SyncSessionService, SyncSessionTransferPlan, SyncSessionWorkspace, WorkspaceRegistryLike, estimateJsonBytes, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateObjectPayload, validateSyncSessionBundle, validateSyncSessionManifest };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { ExportObjectCache, SyncSessionError, SyncSessionService, estimateJsonBytes, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateSyncSessionBundle, validateSyncSessionManifest };
|
|
1
|
+
import { a as estimateJsonBytes, c as planSessionTransfers, d as validateObjectPayload, f as validateSyncSessionBundle, i as SyncSessionService, l as sessionEventListDigest, n as SyncSessionError, o as makeSyncSessionBundle, p as validateSyncSessionManifest, r as SyncSessionObjectInbox, s as makeSyncSessionManifest, t as ExportObjectCache, u as syncSessionEntryKey } from "../../session-sync-CIqjuWkh.js";
|
|
2
|
+
export { ExportObjectCache, SyncSessionError, SyncSessionObjectInbox, SyncSessionService, estimateJsonBytes, makeSyncSessionBundle, makeSyncSessionManifest, planSessionTransfers, sessionEventListDigest, syncSessionEntryKey, validateObjectPayload, validateSyncSessionBundle, validateSyncSessionManifest };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as SyncSshProbeError, c as buildSyncSshConnectConfig, i as SyncSshErrorCategory, n as SyncRemoteNodeManifest, o as SyncSshSession, r as SyncSessionDeltaPrefixError, s as SyncSshTransport, t as SyncRemoteHandshake } from "../../ssh-transport-
|
|
1
|
+
import { a as SyncSshProbeError, c as buildSyncSshConnectConfig, i as SyncSshErrorCategory, n as SyncRemoteNodeManifest, o as SyncSshSession, r as SyncSessionDeltaPrefixError, s as SyncSshTransport, t as SyncRemoteHandshake } from "../../ssh-transport-B2oNDs0D.js";
|
|
2
2
|
export { SyncRemoteHandshake, SyncRemoteNodeManifest, SyncSessionDeltaPrefixError, SyncSshErrorCategory, SyncSshProbeError, SyncSshSession, SyncSshTransport, buildSyncSshConnectConfig };
|
|
@@ -296,11 +296,12 @@ var SyncSshTransport = class {
|
|
|
296
296
|
if (manifest.protocolVersion !== 1 || typeof manifest.instanceId !== "string" || !Array.isArray(manifest.entries) || !Array.isArray(manifest.skipped) || typeof manifest.digest !== "string") throw new SyncSshProbeError("protocol");
|
|
297
297
|
return manifest;
|
|
298
298
|
}
|
|
299
|
-
async fetchSessionEntries(peerId, keys, manifestDigest, prefixes = []) {
|
|
299
|
+
async fetchSessionEntries(peerId, keys, manifestDigest, prefixes = [], deferredObjects = false) {
|
|
300
300
|
const value = await this.fetchDataOperation(peerId, "session-entries", {
|
|
301
301
|
keys,
|
|
302
302
|
manifestDigest,
|
|
303
|
-
prefixes
|
|
303
|
+
prefixes,
|
|
304
|
+
deferredObjects
|
|
304
305
|
});
|
|
305
306
|
if (!isRecord(value) || value.ok !== true || !isRecord(value.bundle)) throw new SyncSshProbeError("helper");
|
|
306
307
|
const bundle = value.bundle;
|
|
@@ -359,6 +360,103 @@ var SyncSshTransport = class {
|
|
|
359
360
|
const fingerprint = this.hostKeyChallenges.acceptObserved(peer);
|
|
360
361
|
await this.peerManager.pinHostKey(peerId, fingerprint);
|
|
361
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* 拉取带外对象流:响应以帧流形式逐块 yield,调用方(收件箱)边收边落盘,
|
|
365
|
+
* 内存 O(单帧)。会话在流结束/出错时关闭(生成器 finally)。
|
|
366
|
+
*/
|
|
367
|
+
async *fetchSessionObjects(peerId, objects) {
|
|
368
|
+
const body = Buffer.from(JSON.stringify({ objects: [...objects] }), "utf8");
|
|
369
|
+
if (body.length > 134217728) throw new SyncSshProbeError("protocol");
|
|
370
|
+
const { peer, session } = await this.openSession(peerId);
|
|
371
|
+
try {
|
|
372
|
+
await session.sendControl({
|
|
373
|
+
kind: "request",
|
|
374
|
+
protocolVersion: 1,
|
|
375
|
+
operation: "session-objects",
|
|
376
|
+
port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT
|
|
377
|
+
});
|
|
378
|
+
for (let offset = 0, sequence = 0; offset < body.length; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) await session.sendData(sequence, body.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, body.length)));
|
|
379
|
+
await session.sendControl({ kind: "request-end" });
|
|
380
|
+
session.endInput();
|
|
381
|
+
let responseStatus;
|
|
382
|
+
let bodyMode;
|
|
383
|
+
let responseEnded = false;
|
|
384
|
+
let totalBytes = 0;
|
|
385
|
+
for await (const frame of session.readFrames()) {
|
|
386
|
+
if (frame.kind === "control") {
|
|
387
|
+
if (frame.value.kind === "error") throw new SyncSshProbeError("helper");
|
|
388
|
+
if (frame.value.kind === "response" && responseStatus === void 0) {
|
|
389
|
+
responseStatus = typeof frame.value.status === "number" ? frame.value.status : void 0;
|
|
390
|
+
bodyMode = typeof frame.value.bodyMode === "string" ? frame.value.bodyMode : void 0;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (frame.value.kind === "response-end" && responseStatus !== void 0 && !responseEnded) {
|
|
394
|
+
responseEnded = true;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
throw new SyncProtocolError("unexpected session-objects control frame");
|
|
398
|
+
}
|
|
399
|
+
if (responseStatus === void 0 || bodyMode !== "data" || responseEnded) throw new SyncProtocolError("unexpected session-objects data frame");
|
|
400
|
+
totalBytes += frame.data.length;
|
|
401
|
+
if (totalBytes > 68719476736) throw new SyncProtocolError("session-objects exceeds object stream limit");
|
|
402
|
+
yield frame.data;
|
|
403
|
+
}
|
|
404
|
+
if (responseStatus !== 200 || !responseEnded) throw new SyncSshProbeError("helper");
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (error instanceof SyncSshProbeError) throw error;
|
|
407
|
+
if (error instanceof SyncProtocolError) {
|
|
408
|
+
const probe = new SyncSshProbeError("protocol");
|
|
409
|
+
probe.detail = error.message;
|
|
410
|
+
throw probe;
|
|
411
|
+
}
|
|
412
|
+
throw new SyncSshProbeError("helper");
|
|
413
|
+
} finally {
|
|
414
|
+
session.close();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* 推送带外对象流:把帧流按 1MB 分块写入请求体,逐块经 SSH 发送;
|
|
419
|
+
* 对端流式落盘收件箱后返回 JSON 摘要。
|
|
420
|
+
*/
|
|
421
|
+
async pushSessionObjects(peerId, frames) {
|
|
422
|
+
const { peer, session } = await this.openSession(peerId);
|
|
423
|
+
try {
|
|
424
|
+
await session.sendControl({
|
|
425
|
+
kind: "request",
|
|
426
|
+
protocolVersion: 1,
|
|
427
|
+
operation: "session-objects-push",
|
|
428
|
+
port: peer.remoteWebPort ?? DEFAULT_REMOTE_WEB_PORT
|
|
429
|
+
});
|
|
430
|
+
let sequence = 0;
|
|
431
|
+
let totalBytes = 0;
|
|
432
|
+
for await (const chunk of frames) {
|
|
433
|
+
if (!(chunk instanceof Uint8Array)) throw new SyncProtocolError("session-objects-push contains a non-byte chunk");
|
|
434
|
+
const data = Buffer.from(chunk);
|
|
435
|
+
totalBytes += data.byteLength;
|
|
436
|
+
if (totalBytes > 68719476736) throw new SyncProtocolError("session-objects-push exceeds object stream limit");
|
|
437
|
+
for (let offset = 0; offset < data.byteLength; offset += MAX_SYNC_DATA_CHUNK_BYTES, sequence += 1) await session.sendData(sequence, data.subarray(offset, Math.min(offset + MAX_SYNC_DATA_CHUNK_BYTES, data.byteLength)));
|
|
438
|
+
}
|
|
439
|
+
await session.sendControl({ kind: "request-end" });
|
|
440
|
+
session.endInput();
|
|
441
|
+
let response;
|
|
442
|
+
for await (const frame of session.readFrames()) {
|
|
443
|
+
if (frame.kind !== "control" || response !== void 0) throw new SyncProtocolError("unexpected session-objects-push frame");
|
|
444
|
+
if (frame.value.kind === "error") throw new SyncSshProbeError("helper");
|
|
445
|
+
if (frame.value.kind === "response") response = frame.value;
|
|
446
|
+
}
|
|
447
|
+
if (!response || response.status !== 200) throw new SyncSshProbeError("helper");
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (error instanceof SyncSshProbeError) throw error;
|
|
450
|
+
if (error instanceof SyncProtocolError) {
|
|
451
|
+
const probe = new SyncSshProbeError("protocol");
|
|
452
|
+
probe.detail = error.message;
|
|
453
|
+
throw probe;
|
|
454
|
+
}
|
|
455
|
+
throw new SyncSshProbeError("helper");
|
|
456
|
+
} finally {
|
|
457
|
+
session.close();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
362
460
|
async fetchDataOperation(peerId, operation, requestBody) {
|
|
363
461
|
const body = requestBody === void 0 ? void 0 : Buffer.from(JSON.stringify(requestBody), "utf8");
|
|
364
462
|
if (body && body.length > 134217728) throw new SyncSshProbeError("protocol");
|