@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.
Files changed (31) hide show
  1. package/lib/core/index.js +6 -1
  2. package/lib/core/index.js.map +1 -1
  3. package/lib/core/sync/coordinator.d.ts +2 -2
  4. package/lib/core/sync/coordinator.js +23 -6
  5. package/lib/core/sync/coordinator.js.map +1 -1
  6. package/lib/core/sync/internal-routes.d.ts +1 -1
  7. package/lib/core/sync/internal-routes.js +80 -4
  8. package/lib/core/sync/internal-routes.js.map +1 -1
  9. package/lib/core/sync/offline-archive.d.ts +1 -1
  10. package/lib/core/sync/offline-archive.js +1 -1
  11. package/lib/core/sync/offline-routes.js +1 -1
  12. package/lib/core/sync/protocol.d.ts +2 -2
  13. package/lib/core/sync/protocol.js +4 -1
  14. package/lib/core/sync/protocol.js.map +1 -1
  15. package/lib/core/sync/remote-helper.js +107 -76
  16. package/lib/core/sync/remote-helper.js.map +1 -1
  17. package/lib/core/sync/routes.d.ts +1 -1
  18. package/lib/core/sync/session-routes.d.ts +1 -1
  19. package/lib/core/sync/session-routes.js +1 -1
  20. package/lib/core/sync/session-sync.d.ts +2 -2
  21. package/lib/core/sync/session-sync.js +2 -2
  22. package/lib/core/sync/ssh-transport.d.ts +1 -1
  23. package/lib/core/sync/ssh-transport.js +100 -2
  24. package/lib/core/sync/ssh-transport.js.map +1 -1
  25. package/lib/{protocol-KaWZArY8.d.ts → protocol-DJ0zyVlB.d.ts} +5 -2
  26. package/lib/{session-sync-Zfo1Q-qT.d.ts → session-sync-BuOJvFxK.d.ts} +54 -2
  27. package/lib/{session-sync-BQt3HXkp.js → session-sync-CIqjuWkh.js} +455 -48
  28. package/lib/session-sync-CIqjuWkh.js.map +1 -0
  29. package/lib/{ssh-transport-Cfb_nwkY.d.ts → ssh-transport-B2oNDs0D.d.ts} +14 -4
  30. package/package.json +1 -1
  31. package/lib/session-sync-BQt3HXkp.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { n as SyncSessionError } from "../../session-sync-BQt3HXkp.js";
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 SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, c as SyncFrame, d as encodeSyncControlFrame, f as encodeSyncDataFrame, i as SYNC_PROTOCOL_VERSION, l as SyncFrameDecoder, n as MAX_SYNC_DATA_CHUNK_BYTES, o as SYNC_SESSION_EVENT_TAIL_CAPABILITY, r as MAX_SYNC_TRANSFER_BYTES, s as SYNC_SESSION_MANIFEST_CAPABILITY, t as MAX_SYNC_CONTROL_FRAME_BYTES, u as SyncProtocolError } from "../../protocol-KaWZArY8.js";
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":"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"}
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 input = []
18
- let inputBytes = 0
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 decodeFrames(buffer) {
42
- const frames = []
43
- let offset = 0
44
- let sequence = 0
45
- let totalData = 0
46
- while (offset < buffer.length) {
47
- if (buffer.length - offset < 5) throw new Error('truncated frame header')
48
- const kind = buffer[offset]
49
- const length = buffer.readUInt32BE(offset + 1)
50
- offset += 5
51
- const maxLength = kind === 0 ? MAX_CONTROL : 36 + MAX_CHUNK
52
- if ((kind !== 0 && kind !== 1) || length > maxLength || length > buffer.length - offset) {
53
- throw new Error('invalid frame length')
54
- }
55
- const payload = buffer.subarray(offset, offset + length)
56
- offset += length
57
- if (kind === 0) {
58
- if (length < 2 || payload[length - 1] !== 10) throw new Error('invalid control frame')
59
- frames.push({ kind: 'control', value: JSON.parse(payload.subarray(0, -1).toString('utf8')) })
60
- } else {
61
- if (length < 36) throw new Error('invalid data frame')
62
- const currentSequence = payload.readUInt32BE(0)
63
- const expected = payload.subarray(4, 36)
64
- const data = payload.subarray(36)
65
- const actual = createHash('sha256').update(data).digest()
66
- if (currentSequence !== sequence || !timingSafeEqual(expected, actual)) throw new Error('invalid data chunk')
67
- sequence += 1
68
- totalData += data.length
69
- if (totalData > MAX_TRANSFER) throw new Error('transfer exceeds size limit')
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 parseRequest(buffer) {
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) => !['kind', 'protocolVersion', 'operation', 'port'].includes(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
- const operation = PATHS[request.operation]
89
- if (request.operation === 'node-apply' || request.operation === 'session-entries' || request.operation === 'session-apply') {
90
- const last = frames[frames.length - 1]
91
- if (!last || last.kind !== 'control' || last.value.kind !== 'request-end') throw new Error('apply request terminator required')
92
- if (frames.slice(1, -1).some((frame) => frame.kind !== 'data')) throw new Error('invalid apply request body')
93
- const chunks = frames.slice(1, -1).map((frame) => frame.data)
94
- return { request, operation, body: Buffer.concat(chunks) }
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 buffer = Buffer.concat(input, inputBytes)
102
- const { request, operation, body } = parseRequest(buffer)
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
- function writeStdout(data) {
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('manifest response body missing')
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('manifest exceeds size limit')
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
- process.stdin.on('data', (chunk) => {
147
- inputBytes += chunk.length
148
- if (inputBytes > MAX_TRANSFER + 2 * MAX_CONTROL) {
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 input = []\nlet inputBytes = 0\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 decodeFrames(buffer) {\n const frames = []\n let offset = 0\n let sequence = 0\n let totalData = 0\n while (offset < buffer.length) {\n if (buffer.length - offset < 5) throw new Error('truncated frame header')\n const kind = buffer[offset]\n const length = buffer.readUInt32BE(offset + 1)\n offset += 5\n const maxLength = kind === 0 ? MAX_CONTROL : 36 + MAX_CHUNK\n if ((kind !== 0 && kind !== 1) || length > maxLength || length > buffer.length - offset) {\n throw new Error('invalid frame length')\n }\n const payload = buffer.subarray(offset, offset + length)\n offset += length\n if (kind === 0) {\n if (length < 2 || payload[length - 1] !== 10) throw new Error('invalid control frame')\n frames.push({ kind: 'control', value: JSON.parse(payload.subarray(0, -1).toString('utf8')) })\n } else {\n if (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 !== sequence || !timingSafeEqual(expected, actual)) throw new Error('invalid data chunk')\n sequence += 1\n totalData += data.length\n if (totalData > MAX_TRANSFER) throw new Error('transfer exceeds size limit')\n frames.push({ kind: 'data', data })\n }\n }\n return frames\n}\n\nfunction parseRequest(buffer) {\n const frames = decodeFrames(buffer)\n const first = frames[0]\n if (!first || first.kind !== 'control') throw new Error('request control frame required')\n const request = first.value\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((key) => !['kind', 'protocolVersion', 'operation', 'port'].includes(key))) {\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 const operation = PATHS[request.operation]\n if (request.operation === 'node-apply' || request.operation === 'session-entries' || request.operation === 'session-apply') {\n const last = frames[frames.length - 1]\n if (!last || last.kind !== 'control' || last.value.kind !== 'request-end') throw new Error('apply request terminator required')\n if (frames.slice(1, -1).some((frame) => frame.kind !== 'data')) throw new Error('invalid apply request body')\n const chunks = frames.slice(1, -1).map((frame) => frame.data)\n return { request, operation, body: Buffer.concat(chunks) }\n }\n if (frames.length !== 1) throw new Error('unexpected helper request frames')\n return { request, operation }\n}\n\nasync function run() {\n const buffer = Buffer.concat(input, inputBytes)\n const { request, operation, body } = parseRequest(buffer)\n const url = 'http://127.0.0.1:' + request.port + operation.path\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 function 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\n if (request.operation === 'node-manifest' || request.operation === 'session-bundle' ||\n request.operation === 'session-manifest' || request.operation === 'session-entries') {\n await writeStdout(encodeControl({ kind: 'response', status: response.status, bodyMode: 'data' }))\n if (!response.body) throw new Error('manifest 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) throw new Error('manifest 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\nprocess.stdin.on('data', (chunk) => {\n inputBytes += chunk.length\n if (inputBytes > MAX_TRANSFER + 2 * MAX_CONTROL) {\n process.exitCode = 1\n process.stdin.destroy()\n return\n }\n input.push(Buffer.from(chunk))\n})\nprocess.stdin.on('end', () => {\n run().catch(() => {\n try { process.stdout.write(encodeControl({ kind: 'error', code: 'helper-failed' })) } catch {}\n process.exitCode = 1\n })\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkKvC,MAAa,6BACX,8BAH2B,OAAO,KAAK,sBAAsB,MAAM,CAAC,CAAC,SAAS,QAGhD,EAAqB"}
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-Cfb_nwkY.js";
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 { p as SyncSessionService } from "../../session-sync-Zfo1Q-qT.js";
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,4 +1,4 @@
1
- import { n as SyncSessionError } from "../../session-sync-BQt3HXkp.js";
1
+ import { n as SyncSessionError } from "../../session-sync-CIqjuWkh.js";
2
2
  //#region src/core/sync/session-routes.ts
3
3
  function sendJson(res, status, body) {
4
4
  res.writeHead(status, {
@@ -1,2 +1,2 @@
1
- import { C as validateSyncSessionBundle, S as syncSessionEntryKey, _ as estimateJsonBytes, a as SyncSessionEntry, b as planSessionTransfers, c as SyncSessionEventPrefix, d as SyncSessionManifestEntry, f as SyncSessionObject, g as WorkspaceRegistryLike, h as SyncSessionWorkspace, i as SyncSessionBundle, l as SyncSessionInstructionSnapshot, m as SyncSessionTransferPlan, n as SyncSessionApplyOptions, o as SyncSessionError, p as SyncSessionService, r as SyncSessionApplyResult, s as SyncSessionEventDelta, t as ExportObjectCache, u as SyncSessionManifest, v as makeSyncSessionBundle, w as validateSyncSessionManifest, x as sessionEventListDigest, y as makeSyncSessionManifest } from "../../session-sync-Zfo1Q-qT.js";
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 makeSyncSessionBundle, c as sessionEventListDigest, d as validateSyncSessionManifest, i as estimateJsonBytes, l as syncSessionEntryKey, n as SyncSessionError, o as makeSyncSessionManifest, r as SyncSessionService, s as planSessionTransfers, t as ExportObjectCache, u as validateSyncSessionBundle } from "../../session-sync-BQt3HXkp.js";
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-Cfb_nwkY.js";
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");