@peers-app/peers-device 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-download-manager.d.ts +14 -0
- package/dist/chunk-download-manager.js +132 -0
- package/dist/chunk-download-manager.js.map +1 -0
- package/dist/chunk-download-manager.test.d.ts +1 -0
- package/dist/chunk-download-manager.test.js +166 -0
- package/dist/chunk-download-manager.test.js.map +1 -0
- package/dist/chunk-download.types.d.ts +15 -0
- package/dist/chunk-download.types.js +3 -0
- package/dist/chunk-download.types.js.map +1 -0
- package/dist/connection-manager/connection-manager.d.ts +32 -0
- package/dist/connection-manager/connection-manager.js +235 -0
- package/dist/connection-manager/connection-manager.js.map +1 -0
- package/dist/connection-manager/least-preferred-connection.d.ts +3 -0
- package/dist/connection-manager/least-preferred-connection.js +45 -0
- package/dist/connection-manager/least-preferred-connection.js.map +1 -0
- package/dist/connection-manager/least-preferred-connection.test.d.ts +1 -0
- package/dist/connection-manager/least-preferred-connection.test.js +300 -0
- package/dist/connection-manager/least-preferred-connection.test.js.map +1 -0
- package/dist/device-sync.d.ts +78 -0
- package/dist/device-sync.js +541 -0
- package/dist/device-sync.js.map +1 -0
- package/dist/device-sync.test.d.ts +1 -0
- package/dist/device-sync.test.js +1618 -0
- package/dist/device-sync.test.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/json-diff.d.ts +9 -0
- package/dist/json-diff.js +137 -0
- package/dist/json-diff.js.map +1 -0
- package/dist/local.data-source.d.ts +19 -0
- package/dist/local.data-source.js +146 -0
- package/dist/local.data-source.js.map +1 -0
- package/dist/local.data-source.test.d.ts +1 -0
- package/dist/local.data-source.test.js +68 -0
- package/dist/local.data-source.test.js.map +1 -0
- package/dist/main.d.ts +4 -0
- package/dist/main.js +138 -0
- package/dist/main.js.map +1 -0
- package/dist/packages.tracked-data-source.d.ts +10 -0
- package/dist/packages.tracked-data-source.js +28 -0
- package/dist/packages.tracked-data-source.js.map +1 -0
- package/dist/pvars.tracked-data-source.d.ts +5 -0
- package/dist/pvars.tracked-data-source.js +54 -0
- package/dist/pvars.tracked-data-source.js.map +1 -0
- package/dist/tracked-data-source.d.ts +46 -0
- package/dist/tracked-data-source.js +387 -0
- package/dist/tracked-data-source.js.map +1 -0
- package/dist/tracked-data-source.test.d.ts +1 -0
- package/dist/tracked-data-source.test.js +825 -0
- package/dist/tracked-data-source.test.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IFileChunkInfo } from "@peers-app/peers-sdk";
|
|
2
|
+
import { ConnectionManager } from "./connection-manager/connection-manager";
|
|
3
|
+
export declare class ChunkDownloadManager {
|
|
4
|
+
private connectionManager;
|
|
5
|
+
private activeChunkUploadCount;
|
|
6
|
+
private pendingRetries;
|
|
7
|
+
constructor(connectionManager: ConnectionManager);
|
|
8
|
+
getLocalChunk(chunkHash: string): Promise<Uint8Array | null>;
|
|
9
|
+
downloadChunk(chunkHash: string): Promise<Uint8Array | null>;
|
|
10
|
+
getChunkInfo(chunkHash: string): Promise<IFileChunkInfo>;
|
|
11
|
+
onNewPeerConnected(): Promise<void>;
|
|
12
|
+
getPendingRetryCount(): number;
|
|
13
|
+
clearPendingRetries(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChunkDownloadManager = void 0;
|
|
4
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
5
|
+
class ChunkDownloadManager {
|
|
6
|
+
connectionManager;
|
|
7
|
+
activeChunkUploadCount = 0;
|
|
8
|
+
pendingRetries = new Set(); // Track chunks that failed to download
|
|
9
|
+
constructor(connectionManager) {
|
|
10
|
+
this.connectionManager = connectionManager;
|
|
11
|
+
}
|
|
12
|
+
async getLocalChunk(chunkHash) {
|
|
13
|
+
// TODO: debounce this method to avoid multiple calls for the same chunk
|
|
14
|
+
// TODO: throttle this method to avoid too many concurrent reads
|
|
15
|
+
try {
|
|
16
|
+
this.activeChunkUploadCount++;
|
|
17
|
+
const fileOps = await (0, peers_sdk_1.getFileOps)();
|
|
18
|
+
const chunkPath = `${peers_sdk_1.CHUNKS_DIR}/${chunkHash}`;
|
|
19
|
+
if (await fileOps.fileExists(chunkPath)) {
|
|
20
|
+
const chunkData = await fileOps.readFile(chunkPath);
|
|
21
|
+
return chunkData;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.warn(`Error reading local chunk ${chunkHash}:`, error);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
this.activeChunkUploadCount--;
|
|
29
|
+
}
|
|
30
|
+
return null; // If we can't read it, assume it's not available
|
|
31
|
+
}
|
|
32
|
+
async downloadChunk(chunkHash) {
|
|
33
|
+
// TODO: debounce this method to avoid multiple calls for the same chunk
|
|
34
|
+
// TODO: throttle this method to avoid too many concurrent downloads
|
|
35
|
+
// check if we already have this chunk locally
|
|
36
|
+
const localChunk = await this.getLocalChunk(chunkHash);
|
|
37
|
+
if (localChunk) {
|
|
38
|
+
console.warn(`Chunk ${chunkHash} already exists locally, returning it: ${chunkHash}`);
|
|
39
|
+
return localChunk; // Return immediately if we have it
|
|
40
|
+
}
|
|
41
|
+
// TODO: if we don't have it locally, check if we're already downloading it
|
|
42
|
+
const downloadDevices = this.connectionManager.getDownloadDevices();
|
|
43
|
+
// Naive algorithm: iterate through each remote device one at a time
|
|
44
|
+
for (const downloadDevice of downloadDevices) {
|
|
45
|
+
try {
|
|
46
|
+
// Check if this device has the chunk
|
|
47
|
+
const chunkInfo = await downloadDevice.getFileChunkInfo(chunkHash);
|
|
48
|
+
if (!chunkInfo?.hasChunk) {
|
|
49
|
+
console.debug(`Chunk info not available for ${chunkHash} on device ${downloadDevice.deviceId}`);
|
|
50
|
+
continue; // Skip to next device
|
|
51
|
+
}
|
|
52
|
+
const chunkData = await downloadDevice.downloadFileChunk(chunkHash);
|
|
53
|
+
if (chunkData) {
|
|
54
|
+
const hash = (0, peers_sdk_1.hashBytes)(chunkData);
|
|
55
|
+
if (hash !== chunkHash) {
|
|
56
|
+
console.warn(`Downloaded chunk ${chunkHash} from device ${downloadDevice.deviceId} has invalid hash: ${hash}`);
|
|
57
|
+
continue; // Try next device
|
|
58
|
+
}
|
|
59
|
+
// Successfully downloaded, remove from retry queue
|
|
60
|
+
this.pendingRetries.delete(chunkHash);
|
|
61
|
+
// TODO: This may result in a double-write. Try to fix later but for now it's better than not saving the chunk at all
|
|
62
|
+
const fileOps = await (0, peers_sdk_1.getFileOps)();
|
|
63
|
+
await fileOps.writeFile(`${peers_sdk_1.CHUNKS_DIR}/${chunkHash}`, chunkData);
|
|
64
|
+
return chunkData;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.warn(`Failed to download chunk ${chunkHash} from device ${downloadDevice.deviceId}:`, error);
|
|
69
|
+
// Continue to next device
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Track this chunk for retry when new peers connect
|
|
73
|
+
this.pendingRetries.add(chunkHash);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
async getChunkInfo(chunkHash) {
|
|
77
|
+
try {
|
|
78
|
+
// Check if we have this chunk locally first
|
|
79
|
+
const fileOps = await (0, peers_sdk_1.getFileOps)();
|
|
80
|
+
const chunkPath = `${peers_sdk_1.CHUNKS_DIR}/${chunkHash}`;
|
|
81
|
+
const hasChunk = await fileOps.fileExists(chunkPath);
|
|
82
|
+
if (hasChunk) {
|
|
83
|
+
// Return info about our current upload queue
|
|
84
|
+
const downloadQueueSize = this.activeChunkUploadCount;
|
|
85
|
+
// Estimate wait time based on current uploads (rough estimate)
|
|
86
|
+
// Assume average chunk upload takes ~200ms per chunk in queue
|
|
87
|
+
const estimatedWaitMs = downloadQueueSize * 200;
|
|
88
|
+
return {
|
|
89
|
+
hasChunk: true,
|
|
90
|
+
downloadQueueSize,
|
|
91
|
+
estimatedWaitMs
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.warn(`Error checking local chunk info for ${chunkHash}:`, error);
|
|
97
|
+
}
|
|
98
|
+
return { hasChunk: false };
|
|
99
|
+
}
|
|
100
|
+
async onNewPeerConnected() {
|
|
101
|
+
// Retry pending chunk downloads when new peers connect
|
|
102
|
+
if (this.pendingRetries.size === 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const retryHashes = Array.from(this.pendingRetries);
|
|
106
|
+
console.debug(`Retrying ${retryHashes.length} failed chunk downloads with new peer`);
|
|
107
|
+
// Process retries in background without blocking
|
|
108
|
+
setTimeout(async () => {
|
|
109
|
+
for (const chunkHash of retryHashes) {
|
|
110
|
+
try {
|
|
111
|
+
const result = await this.downloadChunk(chunkHash);
|
|
112
|
+
if (result) {
|
|
113
|
+
console.debug(`Successfully retried chunk ${chunkHash}`);
|
|
114
|
+
const fileOps = await (0, peers_sdk_1.getFileOps)();
|
|
115
|
+
await fileOps.writeFile(`${peers_sdk_1.CHUNKS_DIR}/${chunkHash}`, result);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.warn(`Retry failed for chunk ${chunkHash}:`, error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, 100); // Small delay to let connection stabilize
|
|
123
|
+
}
|
|
124
|
+
getPendingRetryCount() {
|
|
125
|
+
return this.pendingRetries.size;
|
|
126
|
+
}
|
|
127
|
+
clearPendingRetries() {
|
|
128
|
+
this.pendingRetries.clear();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.ChunkDownloadManager = ChunkDownloadManager;
|
|
132
|
+
//# sourceMappingURL=chunk-download-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-download-manager.js","sourceRoot":"","sources":["../src/chunk-download-manager.ts"],"names":[],"mappings":";;;AAAA,oDAAyF;AAGzF,MAAa,oBAAoB;IAKX;IAHZ,sBAAsB,GAAG,CAAC,CAAC;IAC3B,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC,CAAC,uCAAuC;IAEnF,YAAoB,iBAAoC;QAApC,sBAAiB,GAAjB,iBAAiB,CAAmB;IAAG,CAAC;IAErD,KAAK,CAAC,aAAa,CAAC,SAAiB;QAC1C,wEAAwE;QACxE,gEAAgE;QAChE,IAAI,CAAC;YACH,IAAI,CAAC,sBAAsB,EAAE,CAAC;YAC9B,MAAM,OAAO,GAAG,MAAM,IAAA,sBAAU,GAAE,CAAC;YACnC,MAAM,SAAS,GAAG,GAAG,sBAAU,IAAI,SAAS,EAAE,CAAC;YAE/C,IAAI,MAAM,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBACxC,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;gBACpD,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,6BAA6B,SAAS,GAAG,EAAE,KAAK,CAAC,CAAC;QACjE,CAAC;gBACO,CAAC;YACP,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAChC,CAAC;QACD,OAAO,IAAI,CAAC,CAAC,iDAAiD;IAChE,CAAC;IAEM,KAAK,CAAC,aAAa,CAAC,SAAiB;QAC1C,wEAAwE;QACxE,oEAAoE;QAEpE,8CAA8C;QAC9C,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QACvD,IAAI,UAAU,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,SAAS,SAAS,0CAA0C,SAAS,EAAE,CAAC,CAAC;YACtF,OAAO,UAAU,CAAC,CAAC,mCAAmC;QACxD,CAAC;QAED,2EAA2E;QAE3E,MAAM,eAAe,GAAG,IAAI,CAAC,iBAAiB,CAAC,kBAAkB,EAAE,CAAC;QAEpE,oEAAoE;QACpE,KAAK,MAAM,cAAc,IAAI,eAAe,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,qCAAqC;gBACrC,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;gBACnE,IAAI,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;oBACzB,OAAO,CAAC,KAAK,CAAC,gCAAgC,SAAS,cAAc,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC;oBAChG,SAAS,CAAC,sBAAsB;gBAClC,CAAC;gBACD,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;gBACpE,IAAI,SAAS,EAAE,CAAC;oBACd,MAAM,IAAI,GAAG,IAAA,qBAAS,EAAC,SAAS,CAAC,CAAC;oBAClC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;wBACvB,OAAO,CAAC,IAAI,CAAC,oBAAoB,SAAS,gBAAgB,cAAc,CAAC,QAAQ,sBAAsB,IAAI,EAAE,CAAC,CAAC;wBAC/G,SAAS,CAAC,kBAAkB;oBAC9B,CAAC;oBACD,mDAAmD;oBACnD,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;oBACtC,qHAAqH;oBACrH,MAAM,OAAO,GAAG,MAAM,IAAA,sBAAU,GAAE,CAAC;oBACnC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,sBAAU,IAAI,SAAS,EAAE,EAAE,SAAS,CAAC,CAAC;oBACjE,OAAO,SAAS,CAAC;gBACnB,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,IAAI,CAAC,4BAA4B,SAAS,gBAAgB,cAAc,CAAC,QAAQ,GAAG,EAAE,KAAK,CAAC,CAAC;gBACrG,0BAA0B;YAC5B,CAAC;QACH,CAAC;QAED,oDAAoD;QACpD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,KAAK,CAAC,YAAY,CAAC,SAAiB;QACzC,IAAI,CAAC;YACH,4CAA4C;YAC5C,MAAM,OAAO,GAAG,MAAM,IAAA,sBAAU,GAAE,CAAC;YACnC,MAAM,SAAS,GAAG,GAAG,sBAAU,IAAI,SAAS,EAAE,CAAC;YAC/C,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YACrD,IAAI,QAAQ,EAAE,CAAC;gBACb,6CAA6C;gBAC7C,MAAM,iBAAiB,GAAG,IAAI,CAAC,sBAAsB,CAAC;gBACtD,+DAA+D;gBAC/D,8DAA8D;gBAC9D,MAAM,eAAe,GAAG,iBAAiB,GAAG,GAAG,CAAC;gBAEhD,OAAO;oBACL,QAAQ,EAAE,IAAI;oBACd,iBAAiB;oBACjB,eAAe;iBAChB,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,uCAAuC,SAAS,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC7B,CAAC;IAEM,KAAK,CAAC,kBAAkB;QAC7B,uDAAuD;QACvD,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACnC,OAAO;QACT,CAAC;QAED,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACpD,OAAO,CAAC,KAAK,CAAC,YAAY,WAAW,CAAC,MAAM,uCAAuC,CAAC,CAAC;QAErF,iDAAiD;QACjD,UAAU,CAAC,KAAK,IAAI,EAAE;YACpB,KAAK,MAAM,SAAS,IAAI,WAAW,EAAE,CAAC;gBACpC,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;oBACnD,IAAI,MAAM,EAAE,CAAC;wBACX,OAAO,CAAC,KAAK,CAAC,8BAA8B,SAAS,EAAE,CAAC,CAAC;wBACzD,MAAM,OAAO,GAAG,MAAM,IAAA,sBAAU,GAAE,CAAC;wBACnC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,sBAAU,IAAI,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;oBAChE,CAAC;gBACH,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,OAAO,CAAC,IAAI,CAAC,0BAA0B,SAAS,GAAG,EAAE,KAAK,CAAC,CAAC;gBAC9D,CAAC;YACH,CAAC;QACH,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,0CAA0C;IACrD,CAAC;IAEM,oBAAoB;QACzB,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC;IAClC,CAAC;IAEM,mBAAmB;QACxB,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;IAC9B,CAAC;CACF;AAvID,oDAuIC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chunk_download_manager_1 = require("./chunk-download-manager");
|
|
4
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
5
|
+
// Mock FileOps for testing
|
|
6
|
+
const mockFileOps = {
|
|
7
|
+
downloadFileChunk: jest.fn(),
|
|
8
|
+
fileExists: jest.fn(),
|
|
9
|
+
readFile: jest.fn(),
|
|
10
|
+
writeFile: jest.fn(),
|
|
11
|
+
deletePath: jest.fn()
|
|
12
|
+
};
|
|
13
|
+
describe('ChunkDownloadManager', () => {
|
|
14
|
+
let mockConnectionManager;
|
|
15
|
+
let chunkDownloadManager;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Set up FileOps mock
|
|
18
|
+
(0, peers_sdk_1.setFileOps)(mockFileOps);
|
|
19
|
+
// Create a minimal mock PeerConnectionManager
|
|
20
|
+
mockConnectionManager = {
|
|
21
|
+
getDownloadDevices: jest.fn().mockReturnValue([])
|
|
22
|
+
};
|
|
23
|
+
chunkDownloadManager = new chunk_download_manager_1.ChunkDownloadManager(mockConnectionManager);
|
|
24
|
+
// Reset mocks
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
mockFileOps.fileExists.mockResolvedValue(false); // Default to not having chunks locally
|
|
27
|
+
});
|
|
28
|
+
describe('downloadChunk', () => {
|
|
29
|
+
it('should serve chunk from local storage when available', async () => {
|
|
30
|
+
const chunkData = Buffer.from('local chunk data');
|
|
31
|
+
mockFileOps.fileExists.mockResolvedValue(true);
|
|
32
|
+
mockFileOps.readFile.mockResolvedValue(chunkData);
|
|
33
|
+
const result = await chunkDownloadManager.getLocalChunk('local-chunk');
|
|
34
|
+
expect(result).toBe(chunkData);
|
|
35
|
+
expect(mockFileOps.fileExists).toHaveBeenCalledWith('file_chunks/local-chunk');
|
|
36
|
+
expect(mockFileOps.readFile).toHaveBeenCalledWith('file_chunks/local-chunk');
|
|
37
|
+
});
|
|
38
|
+
it('should return null when no remote devices are available', async () => {
|
|
39
|
+
const result = await chunkDownloadManager.downloadChunk('test-chunk-hash');
|
|
40
|
+
expect(result).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
it('should return null when no device has the chunk', async () => {
|
|
43
|
+
const mockDownloadDevice = {
|
|
44
|
+
deviceId: 'device-1',
|
|
45
|
+
userId: 'user-1',
|
|
46
|
+
getFileChunkInfo: jest.fn().mockResolvedValue({ hasChunk: false }),
|
|
47
|
+
downloadFileChunk: jest.fn()
|
|
48
|
+
};
|
|
49
|
+
mockConnectionManager.getDownloadDevices = jest.fn().mockReturnValue([mockDownloadDevice]);
|
|
50
|
+
const result = await chunkDownloadManager.downloadChunk('test-chunk-hash');
|
|
51
|
+
expect(result).toBeNull();
|
|
52
|
+
expect(mockDownloadDevice.getFileChunkInfo).toHaveBeenCalledWith('test-chunk-hash');
|
|
53
|
+
});
|
|
54
|
+
it('should download chunk from device that has it', async () => {
|
|
55
|
+
const chunkData = Buffer.from('test chunk data');
|
|
56
|
+
const chunkHash = 'NPoJR9ZZzmNDy_5r46HKiC9rIbNSMiEPGUeR1UVEDEA'; // Correct hash for 'test chunk data'
|
|
57
|
+
const mockDownloadDevice = {
|
|
58
|
+
deviceId: 'device-1',
|
|
59
|
+
userId: 'user-1',
|
|
60
|
+
getFileChunkInfo: jest.fn().mockResolvedValue({
|
|
61
|
+
hasChunk: true,
|
|
62
|
+
downloadQueueSize: 0,
|
|
63
|
+
estimatedWaitMs: 0
|
|
64
|
+
}),
|
|
65
|
+
downloadFileChunk: jest.fn().mockResolvedValue(chunkData)
|
|
66
|
+
};
|
|
67
|
+
mockConnectionManager.getDownloadDevices = jest.fn().mockReturnValue([mockDownloadDevice]);
|
|
68
|
+
const result = await chunkDownloadManager.downloadChunk(chunkHash);
|
|
69
|
+
expect(result).toBe(chunkData);
|
|
70
|
+
expect(mockDownloadDevice.getFileChunkInfo).toHaveBeenCalledWith(chunkHash);
|
|
71
|
+
expect(mockDownloadDevice.downloadFileChunk).toHaveBeenCalledWith(chunkHash);
|
|
72
|
+
});
|
|
73
|
+
it('should deduplicate concurrent downloads of the same chunk', async () => {
|
|
74
|
+
const chunkData = Buffer.from('test chunk data');
|
|
75
|
+
const chunkHash = 'NPoJR9ZZzmNDy_5r46HKiC9rIbNSMiEPGUeR1UVEDEA'; // Correct hash for 'test chunk data'
|
|
76
|
+
const mockDownloadDevice = {
|
|
77
|
+
deviceId: 'device-1',
|
|
78
|
+
userId: 'user-1',
|
|
79
|
+
getFileChunkInfo: jest.fn().mockResolvedValue({
|
|
80
|
+
hasChunk: true,
|
|
81
|
+
downloadQueueSize: 0,
|
|
82
|
+
estimatedWaitMs: 0
|
|
83
|
+
}),
|
|
84
|
+
downloadFileChunk: jest.fn().mockResolvedValue(chunkData)
|
|
85
|
+
};
|
|
86
|
+
mockConnectionManager.getDownloadDevices = jest.fn().mockReturnValue([mockDownloadDevice]);
|
|
87
|
+
// Start two concurrent downloads
|
|
88
|
+
const [result1, result2] = await Promise.all([
|
|
89
|
+
chunkDownloadManager.downloadChunk(chunkHash),
|
|
90
|
+
chunkDownloadManager.downloadChunk(chunkHash)
|
|
91
|
+
]);
|
|
92
|
+
expect(result1).toBe(chunkData);
|
|
93
|
+
expect(result2).toBe(chunkData);
|
|
94
|
+
// Note: Deduplication not yet implemented, so this will call twice for now
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('getChunkInfo', () => {
|
|
98
|
+
it('should return local chunk info when chunk exists locally', async () => {
|
|
99
|
+
mockFileOps.fileExists.mockResolvedValue(true);
|
|
100
|
+
const result = await chunkDownloadManager.getChunkInfo('local-chunk');
|
|
101
|
+
expect(result).toEqual({
|
|
102
|
+
hasChunk: true,
|
|
103
|
+
downloadQueueSize: 0, // No active uploads
|
|
104
|
+
estimatedWaitMs: 0 // No wait time
|
|
105
|
+
});
|
|
106
|
+
expect(mockFileOps.fileExists).toHaveBeenCalledWith('file_chunks/local-chunk');
|
|
107
|
+
});
|
|
108
|
+
it('should return hasChunk: false when chunk not available locally', async () => {
|
|
109
|
+
mockFileOps.fileExists.mockResolvedValue(false);
|
|
110
|
+
const result = await chunkDownloadManager.getChunkInfo('remote-chunk');
|
|
111
|
+
expect(result).toEqual({ hasChunk: false });
|
|
112
|
+
expect(mockFileOps.fileExists).toHaveBeenCalledWith('file_chunks/remote-chunk');
|
|
113
|
+
});
|
|
114
|
+
it('should return hasChunk: false when no device has the chunk', async () => {
|
|
115
|
+
mockFileOps.fileExists.mockResolvedValue(false);
|
|
116
|
+
const mockDownloadDevice = {
|
|
117
|
+
deviceId: 'device-1',
|
|
118
|
+
userId: 'user-1',
|
|
119
|
+
getFileChunkInfo: jest.fn().mockResolvedValue({ hasChunk: false }),
|
|
120
|
+
downloadFileChunk: jest.fn()
|
|
121
|
+
};
|
|
122
|
+
mockConnectionManager.getDownloadDevices = jest.fn().mockReturnValue([mockDownloadDevice]);
|
|
123
|
+
const result = await chunkDownloadManager.getChunkInfo('missing-chunk');
|
|
124
|
+
expect(result).toEqual({ hasChunk: false });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe('retry mechanism', () => {
|
|
128
|
+
it('should track pending retries when downloads fail', async () => {
|
|
129
|
+
expect(chunkDownloadManager.getPendingRetryCount()).toBe(0);
|
|
130
|
+
// Try to download a chunk that doesn't exist anywhere
|
|
131
|
+
const result = await chunkDownloadManager.downloadChunk('missing-chunk');
|
|
132
|
+
expect(result).toBeNull();
|
|
133
|
+
expect(chunkDownloadManager.getPendingRetryCount()).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
it('should retry pending chunks when new peer connects', async () => {
|
|
136
|
+
const chunkHash = 'NPoJR9ZZzmNDy_5r46HKiC9rIbNSMiEPGUeR1UVEDEA';
|
|
137
|
+
const chunkData = Buffer.from('test chunk data');
|
|
138
|
+
// First attempt with no peers - should fail and add to retry queue
|
|
139
|
+
await chunkDownloadManager.downloadChunk(chunkHash);
|
|
140
|
+
expect(chunkDownloadManager.getPendingRetryCount()).toBe(1);
|
|
141
|
+
// Now add a peer that has the chunk
|
|
142
|
+
const mockDownloadDevice = {
|
|
143
|
+
deviceId: 'device-1',
|
|
144
|
+
userId: 'user-1',
|
|
145
|
+
getFileChunkInfo: jest.fn().mockResolvedValue({
|
|
146
|
+
hasChunk: true,
|
|
147
|
+
downloadQueueSize: 0,
|
|
148
|
+
estimatedWaitMs: 0
|
|
149
|
+
}),
|
|
150
|
+
downloadFileChunk: jest.fn().mockResolvedValue(chunkData)
|
|
151
|
+
};
|
|
152
|
+
mockConnectionManager.getDownloadDevices = jest.fn().mockReturnValue([mockDownloadDevice]);
|
|
153
|
+
// Trigger retry when new peer connects
|
|
154
|
+
await chunkDownloadManager.onNewPeerConnected();
|
|
155
|
+
// Give the async retry a moment to complete
|
|
156
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
157
|
+
// Should have attempted to retry the chunk
|
|
158
|
+
expect(mockDownloadDevice.getFileChunkInfo).toHaveBeenCalledWith(chunkHash);
|
|
159
|
+
});
|
|
160
|
+
it('should clear pending retries', () => {
|
|
161
|
+
chunkDownloadManager.clearPendingRetries();
|
|
162
|
+
expect(chunkDownloadManager.getPendingRetryCount()).toBe(0);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
//# sourceMappingURL=chunk-download-manager.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-download-manager.test.js","sourceRoot":"","sources":["../src/chunk-download-manager.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ChunkDownloadRequest {
|
|
2
|
+
chunkHash: string;
|
|
3
|
+
resolve: (chunk: Uint8Array | null) => void;
|
|
4
|
+
reject: (error: Error) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface ChunkDownloadPool {
|
|
7
|
+
activeDownloads: Map<string, Promise<Uint8Array | null>>;
|
|
8
|
+
downloadQueue: ChunkDownloadRequest[];
|
|
9
|
+
concurrentLimit: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ChunkDownloadError extends Error {
|
|
12
|
+
chunkHash: string;
|
|
13
|
+
peerDeviceId?: string;
|
|
14
|
+
code: 'CHUNK_NOT_FOUND' | 'NETWORK_ERROR' | 'VERIFICATION_FAILED' | 'TIMEOUT';
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-download.types.js","sourceRoot":"","sources":["../src/chunk-download.types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ChangeTrackingTable, Connection, DataContext, IFileChunkInfo, UserContext } from "@peers-app/peers-sdk";
|
|
2
|
+
import { ChunkDownloadManager } from "../chunk-download-manager";
|
|
3
|
+
import { DeviceSync } from "../device-sync";
|
|
4
|
+
interface IDownloadDevice {
|
|
5
|
+
userId: string;
|
|
6
|
+
deviceId: string;
|
|
7
|
+
getFileChunkInfo(chunkHash: string): Promise<IFileChunkInfo>;
|
|
8
|
+
downloadFileChunk(chunkHash: string): Promise<Uint8Array | null>;
|
|
9
|
+
}
|
|
10
|
+
export declare class ConnectionManager {
|
|
11
|
+
readonly userContext: UserContext;
|
|
12
|
+
readonly changeTrackingTableFactory: (groupId?: string) => ChangeTrackingTable;
|
|
13
|
+
static readonly MAX_CONNECTIONS: 30;
|
|
14
|
+
readonly chunkDownloadManager: ChunkDownloadManager;
|
|
15
|
+
private peerGroupDevices;
|
|
16
|
+
private connectionStates;
|
|
17
|
+
private deviceSyncConnections;
|
|
18
|
+
private groupMemberSubscriptionCleanups;
|
|
19
|
+
constructor(userContext: UserContext, changeTrackingTableFactory: (groupId?: string) => ChangeTrackingTable);
|
|
20
|
+
private setupGroupsSubscriptions;
|
|
21
|
+
private setupGroupMembersSubscription;
|
|
22
|
+
private setConnectionMembership;
|
|
23
|
+
private getConnectionState;
|
|
24
|
+
addConnection(connection: Connection): Promise<void>;
|
|
25
|
+
removeConnection(connection: Connection): void;
|
|
26
|
+
getPeerGroupDevice(dataContext: DataContext): DeviceSync;
|
|
27
|
+
downloadFileChunk(chunkHash: string): Promise<Uint8Array | null>;
|
|
28
|
+
getFileChunkInfo(chunkHash: string): Promise<IFileChunkInfo>;
|
|
29
|
+
getDownloadDevices(): IDownloadDevice[];
|
|
30
|
+
removeLeastPreferredConnection(): void;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectionManager = void 0;
|
|
4
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
5
|
+
const chunk_download_manager_1 = require("../chunk-download-manager");
|
|
6
|
+
const least_preferred_connection_1 = require("./least-preferred-connection");
|
|
7
|
+
const device_sync_1 = require("../device-sync");
|
|
8
|
+
class ConnectionManager {
|
|
9
|
+
userContext;
|
|
10
|
+
changeTrackingTableFactory;
|
|
11
|
+
// TODO set this based on device type (desktops and servers should be able to handle 100 connections)
|
|
12
|
+
static MAX_CONNECTIONS = peers_sdk_1.PeerDeviceConsts.MAX_CONNECTIONS;
|
|
13
|
+
chunkDownloadManager;
|
|
14
|
+
peerGroupDevices = {};
|
|
15
|
+
connectionStates = new Map();
|
|
16
|
+
deviceSyncConnections = new Map();
|
|
17
|
+
groupMemberSubscriptionCleanups = new Map();
|
|
18
|
+
constructor(userContext, changeTrackingTableFactory) {
|
|
19
|
+
this.userContext = userContext;
|
|
20
|
+
this.changeTrackingTableFactory = changeTrackingTableFactory;
|
|
21
|
+
this.chunkDownloadManager = new chunk_download_manager_1.ChunkDownloadManager(this);
|
|
22
|
+
this.setupGroupsSubscriptions();
|
|
23
|
+
}
|
|
24
|
+
async setupGroupsSubscriptions() {
|
|
25
|
+
// the groups the local user is part of is stored in their personal db
|
|
26
|
+
const groupsTable = (0, peers_sdk_1.Groups)(this.userContext.userDataContext);
|
|
27
|
+
groupsTable.dataChanged.subscribe(evt => {
|
|
28
|
+
if (evt.op === 'delete' || evt.dataObject.disabled) {
|
|
29
|
+
this.groupMemberSubscriptionCleanups.get(evt.dataObject.groupId)?.();
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.setupGroupMembersSubscription(evt.dataObject.groupId);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
this.getPeerGroupDevice(this.userContext.userDataContext); // ensure userSyncDevice is created and peerDevice is set on dataLocks table
|
|
36
|
+
for (const groupId of this.userContext.groupIds()) {
|
|
37
|
+
await this.setupGroupMembersSubscription(groupId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async setupGroupMembersSubscription(groupId) {
|
|
41
|
+
if (this.groupMemberSubscriptionCleanups.has(groupId)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// group membership is stored in the group db
|
|
45
|
+
const groupDataContext = this.userContext.getDataContext(groupId);
|
|
46
|
+
const groupMembersTable = (0, peers_sdk_1.GroupMembers)(groupDataContext);
|
|
47
|
+
const subscription = groupMembersTable.dataChanged.subscribe(evt => {
|
|
48
|
+
const membership = evt.dataObject;
|
|
49
|
+
const remoteUserId = membership.userId;
|
|
50
|
+
const remove = evt.op === 'delete';
|
|
51
|
+
const userConnections = Array.from(this.connectionStates.keys())
|
|
52
|
+
.filter(c => c.remoteDeviceInfo.userId === remoteUserId);
|
|
53
|
+
userConnections.forEach(connection => {
|
|
54
|
+
this.setConnectionMembership(connection, membership, remove);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
const dispose = () => {
|
|
58
|
+
subscription.unsubscribe();
|
|
59
|
+
this.groupMemberSubscriptionCleanups.delete(groupDataContext.dataContextId);
|
|
60
|
+
};
|
|
61
|
+
this.groupMemberSubscriptionCleanups.set(groupDataContext.dataContextId, dispose);
|
|
62
|
+
}
|
|
63
|
+
setConnectionMembership(connection, membership, remove, dataContext) {
|
|
64
|
+
const connectionState = this.getConnectionState(connection);
|
|
65
|
+
const groupId = membership.groupId;
|
|
66
|
+
const deviceId = connection.remoteDeviceInfo.deviceId;
|
|
67
|
+
const groupId_deviceId = `${groupId}_${deviceId}`;
|
|
68
|
+
let remoteDevice = connectionState.remoteDevices[groupId_deviceId];
|
|
69
|
+
if (remove) {
|
|
70
|
+
const disposeFn = connectionState.disposeFns[groupId];
|
|
71
|
+
disposeFn?.();
|
|
72
|
+
delete connectionState.remoteDevices[groupId_deviceId];
|
|
73
|
+
}
|
|
74
|
+
else if (remoteDevice) {
|
|
75
|
+
remoteDevice.role = membership.role;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
dataContext ??= this.userContext.getDataContext(membership.groupId);
|
|
79
|
+
const syncDevice = this.getPeerGroupDevice(dataContext);
|
|
80
|
+
connection.exposeRPC(`getNetworkInfo_${groupId}`, () => syncDevice.getNetworkInfo());
|
|
81
|
+
connection.exposeRPC(`listChanges_${groupId}`, (...args) => syncDevice.listChanges(...args));
|
|
82
|
+
connection.exposeRPC(`notifyOfChanges_${groupId}`, (...args) => syncDevice.notifyOfChanges(args[0], args[1]));
|
|
83
|
+
remoteDevice = {
|
|
84
|
+
deviceId,
|
|
85
|
+
userId: connection.remoteDeviceInfo.userId,
|
|
86
|
+
role: membership.role,
|
|
87
|
+
getNetworkInfo: () => connection.emit(`getNetworkInfo_${groupId}`),
|
|
88
|
+
listChanges: (...args) => connection.emit(`listChanges_${groupId}`, ...args),
|
|
89
|
+
notifyOfChanges: (...args) => connection.emit(`notifyOfChanges_${groupId}`, ...args),
|
|
90
|
+
};
|
|
91
|
+
connectionState.remoteDevices[groupId_deviceId] = remoteDevice;
|
|
92
|
+
let deviceSyncConnection;
|
|
93
|
+
const disposeFn = async () => {
|
|
94
|
+
connection.removeAllListeners(`getNetworkInfo_${groupId}`);
|
|
95
|
+
connection.removeAllListeners(`listChanges_${groupId}`);
|
|
96
|
+
connection.removeAllListeners(`notifyOfChanges_${groupId}`);
|
|
97
|
+
delete connectionState.remoteDevices[groupId_deviceId];
|
|
98
|
+
syncDevice.removeConnection(deviceId);
|
|
99
|
+
delete connectionState.disposeFns[groupId];
|
|
100
|
+
if (deviceSyncConnection) {
|
|
101
|
+
this.deviceSyncConnections.delete(deviceSyncConnection);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
connectionState.disposeFns[groupId] = disposeFn;
|
|
105
|
+
syncDevice.addConnection(remoteDevice, { onClose: disposeFn }).then(_deviceSyncConnection => {
|
|
106
|
+
deviceSyncConnection = _deviceSyncConnection;
|
|
107
|
+
this.deviceSyncConnections.set(deviceSyncConnection, connection);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
getConnectionState(connection) {
|
|
112
|
+
if (!this.connectionStates.has(connection)) {
|
|
113
|
+
this.connectionStates.set(connection, {
|
|
114
|
+
remoteUserId: connection.remoteDeviceInfo.userId,
|
|
115
|
+
remoteDevices: {},
|
|
116
|
+
disposeFns: {},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return this.connectionStates.get(connection);
|
|
120
|
+
}
|
|
121
|
+
async addConnection(connection) {
|
|
122
|
+
const remoteDeviceId = connection.remoteDeviceInfo.deviceId;
|
|
123
|
+
if (this.connectionStates.has(connection)) {
|
|
124
|
+
console.error(`Connection has already been added`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const existingConnections = Array.from(this.connectionStates.keys());
|
|
128
|
+
const oldConnection = existingConnections.find(c => c.remoteDeviceInfo.deviceId === remoteDeviceId);
|
|
129
|
+
if (oldConnection) {
|
|
130
|
+
// remove the old connection and continue on adding this new connection
|
|
131
|
+
this.removeConnection(oldConnection);
|
|
132
|
+
}
|
|
133
|
+
else if (existingConnections.length >= ConnectionManager.MAX_CONNECTIONS) {
|
|
134
|
+
this.removeLeastPreferredConnection();
|
|
135
|
+
}
|
|
136
|
+
// setup file download RPCs (these are not group-specific)
|
|
137
|
+
connection.exposeRPC('getFileChunkInfo', (chunkHash) => this.getFileChunkInfo(chunkHash));
|
|
138
|
+
connection.exposeRPC('downloadFileChunk', (chunkHash) => this.chunkDownloadManager.getLocalChunk(chunkHash));
|
|
139
|
+
// Notify chunk manager about new peer for retry attempts
|
|
140
|
+
this.chunkDownloadManager.onNewPeerConnected().catch((err) => {
|
|
141
|
+
console.warn('Error retrying chunk downloads with new peer:', err);
|
|
142
|
+
});
|
|
143
|
+
const remoteUserId = connection.remoteDeviceInfo.userId;
|
|
144
|
+
if (remoteUserId === this.userContext.userId()) {
|
|
145
|
+
this.setConnectionMembership(connection, {
|
|
146
|
+
groupMemberId: '',
|
|
147
|
+
userId: this.userContext.userId(),
|
|
148
|
+
groupId: this.userContext.userId(),
|
|
149
|
+
role: peers_sdk_1.GroupMemberRole.Founder,
|
|
150
|
+
signature: ''
|
|
151
|
+
}, false, this.userContext.userDataContext);
|
|
152
|
+
}
|
|
153
|
+
for (const dataContext of this.userContext.groupDataContexts.values()) {
|
|
154
|
+
let membership = await (0, peers_sdk_1.GroupMembers)(dataContext).findOne({ userId: remoteUserId, groupId: dataContext.dataContextId });
|
|
155
|
+
if (!membership && dataContext.groupId) {
|
|
156
|
+
const role = await (0, peers_sdk_1.getUserRole)(dataContext.groupId, remoteUserId);
|
|
157
|
+
if (role) {
|
|
158
|
+
membership = {
|
|
159
|
+
groupMemberId: '',
|
|
160
|
+
userId: remoteUserId,
|
|
161
|
+
groupId: dataContext.dataContextId,
|
|
162
|
+
role,
|
|
163
|
+
signature: ''
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (membership) {
|
|
168
|
+
if (membership.role >= peers_sdk_1.GroupMemberRole.Writer) {
|
|
169
|
+
this.setConnectionMembership(connection, membership);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// TODO special logic so < Writer can propagate signed blocks of changes
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
removeConnection(connection) {
|
|
178
|
+
const connectionState = this.connectionStates.get(connection);
|
|
179
|
+
if (!connectionState) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const disposeFn of Object.values(connectionState.disposeFns)) {
|
|
183
|
+
try {
|
|
184
|
+
disposeFn();
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
console.error('Error disposing connection:', error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.connectionStates.delete(connection);
|
|
191
|
+
}
|
|
192
|
+
getPeerGroupDevice(dataContext) {
|
|
193
|
+
if (!this.peerGroupDevices[dataContext.dataContextId]) {
|
|
194
|
+
const changeTrackingTable = this.changeTrackingTableFactory(dataContext.groupId);
|
|
195
|
+
const peerDevice = new device_sync_1.DeviceSync(dataContext, changeTrackingTable);
|
|
196
|
+
this.peerGroupDevices[dataContext.dataContextId] = peerDevice;
|
|
197
|
+
// TODO Data Locks will probably need to be redone and this might be able to be removed.
|
|
198
|
+
const dataLocksTable = (0, peers_sdk_1.DataLocks)(dataContext);
|
|
199
|
+
dataLocksTable.peerDevice = peerDevice;
|
|
200
|
+
}
|
|
201
|
+
return this.peerGroupDevices[dataContext.dataContextId];
|
|
202
|
+
}
|
|
203
|
+
async downloadFileChunk(chunkHash) {
|
|
204
|
+
return this.chunkDownloadManager.downloadChunk(chunkHash);
|
|
205
|
+
}
|
|
206
|
+
async getFileChunkInfo(chunkHash) {
|
|
207
|
+
return this.chunkDownloadManager.getChunkInfo(chunkHash);
|
|
208
|
+
}
|
|
209
|
+
getDownloadDevices() {
|
|
210
|
+
return Array.from(this.connectionStates.keys()).map(connection => ({
|
|
211
|
+
userId: connection.remoteDeviceInfo.userId,
|
|
212
|
+
deviceId: connection.remoteDeviceInfo.deviceId,
|
|
213
|
+
getFileChunkInfo: (chunkHash) => connection.emit('getFileChunkInfo', chunkHash),
|
|
214
|
+
downloadFileChunk: (chunkHash) => connection.emit('downloadFileChunk', chunkHash),
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
removeLeastPreferredConnection() {
|
|
218
|
+
const devices = Object.values(this.peerGroupDevices);
|
|
219
|
+
const deviceSyncConnection = (0, least_preferred_connection_1.getLeastPreferredManagerConnection)(devices);
|
|
220
|
+
if (deviceSyncConnection) {
|
|
221
|
+
const connection = this.deviceSyncConnections.get(deviceSyncConnection);
|
|
222
|
+
if (connection) {
|
|
223
|
+
this.removeConnection(connection);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.warn('Was not able to find connection for least-preferred device');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.warn('Was not able to find device for least-preferred connection');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
exports.ConnectionManager = ConnectionManager;
|
|
235
|
+
//# sourceMappingURL=connection-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-manager.js","sourceRoot":"","sources":["../../src/connection-manager/connection-manager.ts"],"names":[],"mappings":"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"}
|