egos-transfer 0.2.3 → 0.2.5

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/CLAUDE.md ADDED
@@ -0,0 +1,79 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ npm run build # TypeScript compilation (tsc)
9
+ npm run watch # TypeScript in watch mode
10
+ ```
11
+
12
+ There is no test suite configured yet. The project uses ESLint + Prettier for linting/formatting (configured via `.eslintrc.js`).
13
+
14
+ ## Architecture
15
+
16
+ A peer-to-peer file transfer system built on **PeerJS** (WebRTC). The core is an abstract `PeerTransfer` class that consumers subclass to provide storage, auth, and API token resolution.
17
+
18
+ ### Layer model
19
+
20
+ ```
21
+ PeerTransfer (abstract, index.ts)
22
+ ├── owns a PeerJS Peer + connection pool (Map<peerId, DataConnection>)
23
+ ├── delegates inbound messages to PeerRouter (router.ts)
24
+ └── provides request/reply over DataConnections with timeout + retry
25
+
26
+ Handler (handler.ts)
27
+ └── registers standard file transfer route handlers on a PeerTransfer
28
+ (messages, file metadata, chunk fetch, pause/resume/cancel, auth tokens, etc.)
29
+
30
+ FileTransfer (file-transfer.ts)
31
+ └── chunked file pull: fetches metadata → registers task → pulls chunks
32
+ in parallel (up to 5 concurrent) → verifies checksum → marks done
33
+
34
+ PeerServer (server.ts)
35
+ └── same router + request/reply pattern but for server-side message handling
36
+ ```
37
+
38
+ ### Key files
39
+
40
+ | File | Role |
41
+ |---|---|
42
+ | `src/index.ts` | Abstract `PeerTransfer` — peer lifecycle, connect pool, request/reply, reconnection with exponential backoff |
43
+ | `src/router.ts` | Express-style router with `path-to-regexp` matching, middleware composition, route caching, performance stats |
44
+ | `src/server.ts` | `PeerServer` — an alternative server-side handler with its own router |
45
+ | `src/handler.ts` | `Handler` — registers concrete route handlers (file ops, messages, previews) that consumers wire to their own request processing |
46
+ | `src/file-transfer.ts` | `FileTransfer` — chunked file pull with progress callbacks, retry with backoff, pause/resume/cancel |
47
+ | `src/http-transfer.ts` | Abstract `HttpTransfer` — HTTP-based chunked upload (alternative transport to P2P) |
48
+ | `src/http-transformer.ts` | Converts `PeerRequestPayload` to Axios config for HTTP fallback |
49
+ | `src/locker.ts` | In-memory distributed lock with queueing, expiry cleanup, and event emission. Singleton `globalLocker` exported |
50
+ | `src/utils.ts` | Utility classes: `NetworkAnalyzer`, `TransferQueueManager`, `ProgressCalculator`, `ErrorHandler`, `FileUtils`, `PerformanceMonitor`, `ConfigManager`, `TransferEventEmitter` |
51
+ | `src/entity.ts` | All enums, constants, and shared types (`PeerRoutes`, `MessageStatus`, `TransferPriority`, `NetworkQuality`, etc.) |
52
+ | `src/constant.ts` | Runtime constants (timeouts, connection error strings) |
53
+ | `src/helper.ts` | `isRandomPeerId` / `purifyDeviceId` |
54
+
55
+ ### Message flow
56
+
57
+ 1. Messages arrive on a `DataConnection` via PeerJS `data` event
58
+ 2. `PeerTransfer.bindConnectionData` dispatches by `message.scope`:
59
+ - `REQUEST` → `router.run(conn, message)` → matched route handler → `responseOk`/`responseFail`
60
+ - `RESPONSE` → looks up `requestId` in `this.requests` map and resolves the pending promise
61
+ 3. `request(peerId, payload)` connects (or reuses pooled connection), sends, and returns a promise that resolves on response or rejects on timeout
62
+
63
+ ### Connection management
64
+
65
+ - Connection pool (`connectPool: Map<string, DataConnection>`) tracks open connections
66
+ - Static `PeerTransfer.connecting` map deduplicates concurrent connection attempts to the same peer
67
+ - `checkConnection` (heartbeat loop) pings all connected peers, removes dead connections, handles TURN credential expiry
68
+ - Reconnection uses exponential backoff (1s base, 30s max), attempts `peer.reconnect()` first before full `createPeer()`
69
+
70
+ ### `PeerTransfer` is abstract
71
+
72
+ Subclasses must implement: `getStore()`, `online()`, `offline()`, `genRequestId()`, `getApi()`, `discoverDeviceById()`, `onDestroy()`, `requireAuth()`, `onConnectLimit()`, `reloadConfig()`.
73
+
74
+ ### Key patterns
75
+
76
+ - **Request/reply**: Every message has a `requestId`. Requests register a callback in `this.requests`; responses resolve it. Unresolved requests are cleaned up by timer (`requestTimers` map).
77
+ - **`inRequest` tracking**: Active requests track their connection; if the connection closes, a fast reconnect (5s) is attempted. If it fails, the request is settled with an error so the retry loop in `request()` can re-issue.
78
+ - **Duplicate connection resolution**: When both peers connect simultaneously, the connection with the lexicographically larger `connectionId` wins — deterministic without timestamps.
79
+ - **Route registration**: `transfer.addRoute({ path, method, callback })` registers a handler. `transfer.use(fn, prefix)` adds middleware scoped by registration order.
package/dist/handler.js CHANGED
@@ -24,6 +24,7 @@ class Handler {
24
24
  path: entity_1.PeerRoutes.MESSAGE,
25
25
  method: 'post',
26
26
  callback: (conn, message) => __awaiter(this, void 0, void 0, function* () {
27
+ var _a;
27
28
  try {
28
29
  const data = {
29
30
  id: message.id,
@@ -33,7 +34,7 @@ class Handler {
33
34
  body: message.body,
34
35
  createdAt: message.createdAt,
35
36
  action: entity_1.PeerAction.RECEIVE,
36
- status: entity_1.MessageStatus.DONE,
37
+ status: ((_a = message.body) === null || _a === void 0 ? void 0 : _a.status) || entity_1.MessageStatus.DONE,
37
38
  };
38
39
  yield handler.process(data);
39
40
  yield transfer.responseOk(conn, message);
package/dist/index.d.ts CHANGED
@@ -18,24 +18,31 @@ export declare abstract class PeerTransfer {
18
18
  resolve: (value: any | PromiseLike<any>) => void;
19
19
  reject: (reason?: any) => void;
20
20
  }>;
21
- protected retry: number;
22
21
  protected peerConfig: PeerTransferOptions | undefined;
23
- deviceId: string;
22
+ readonly deviceId: string;
23
+ /** 接入 peerjs 服务端的 peerId,默认与 deviceId 相同 */
24
+ readonly peerId: string;
24
25
  protected requests: Map<string, ResponseCb>;
26
+ /** 追踪 addRequest 的延时清理定时器,避免重试时误删新回调 */
27
+ protected requestTimers: Map<string, ReturnType<typeof setTimeout>>;
25
28
  protected timeout: number;
26
29
  protected router: PeerRouter;
27
- protected booted: Promise<Peer | undefined>;
30
+ protected booted: Promise<Peer | undefined> | undefined;
28
31
  protected reconnectingPromise: Promise<Peer | undefined> | undefined;
29
- protected checkerId: ReturnType<typeof setInterval>;
32
+ protected checkerId: ReturnType<typeof setInterval> | null;
30
33
  isPaused: boolean;
31
- peerId: string;
34
+ /** 重连尝试次数,成功连接后归零 */
35
+ protected reconnectAttempt: number;
36
+ /** 标记实例已销毁,防止异步操作继续 */
37
+ protected destroyed: boolean;
38
+ /** 按 requestId 追踪进行中的请求 */
32
39
  private inRequest;
33
- constructor(deviceId: string, config: PeerTransferOptions, isRadomPeerId?: boolean);
40
+ constructor(deviceId: string, config: PeerTransferOptions, isRandomPeerId?: boolean);
34
41
  abstract getStore(): any;
35
42
  abstract online(deviceId: string): void;
36
43
  abstract offline(deviceId: string): void;
37
44
  abstract genRequestId(): Promise<string>;
38
- abstract getApi(deviceId: string): Promise<InternalApi | undefined>;
45
+ abstract getApi(peerId: string): Promise<InternalApi | undefined>;
39
46
  abstract discoverDeviceById(deviceId: string): Promise<void>;
40
47
  abstract onDestroy(): void;
41
48
  abstract requireAuth(): Promise<void>;
@@ -50,15 +57,34 @@ export declare abstract class PeerTransfer {
50
57
  onDeviceOnline(message: IMessage): Promise<void>;
51
58
  onDeviceOffline(message: IMessage): void;
52
59
  removeConnection(peerId: string, connectionId?: string): void;
60
+ /**
61
+ * 连接关闭时尝试快速重连(5s 超时):
62
+ * - 重连成功 → 保留 inRequest 条目,更新 conn 引用
63
+ * - 重连失败 → 触发 feedback 并清理 inRequest
64
+ */
65
+ private handleConnectionClose;
53
66
  addConnection(conn: DataConnection): void;
54
67
  checkConnection(): void;
55
68
  getApiHost(): string;
56
69
  getDeviceStatus(peerId: string): Promise<IDeviceStatus | undefined>;
57
- connectToPeer(deviceId: string): Promise<DataConnection | undefined>;
70
+ connectToPeer(peerId: string): Promise<DataConnection | undefined>;
58
71
  private createConnection;
72
+ /**
73
+ * 心跳检测 - 直接 send,不走 request 管线,避免重试放大
74
+ */
59
75
  ping(peerId: string): Promise<boolean>;
60
76
  onConnect(conn: DataConnection): Promise<DataConnection | undefined>;
77
+ /** 绑定 DataConnection 的 data 事件 */
78
+ private bindConnectionData;
61
79
  request(peerId: string, payload: PeerRequestPayload): Promise<PeerMessage>;
80
+ /**
81
+ * 执行一次请求,返回带错误码的结果供外层判断是否重试
82
+ * @param peerId - 目标 peerId
83
+ * @param payload - 请求负载
84
+ * @param requestId - 请求ID(重试时复用)
85
+ * @returns error.code: CONN_CLOSED/CONN_FAILED(可重试)、TIMEOUT(不重试)
86
+ */
87
+ private doRequestOnce;
62
88
  reply(conn: DataConnection, oldMessage: PeerMessage, message: {
63
89
  body?: Record<string, any>;
64
90
  headers?: Record<string, string | number>;