@zoebuildsai/trace 1.5.0
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/.gitignore +115 -0
- package/.trace/progress.json +22 -0
- package/README.md +466 -0
- package/RELEASE-NOTES-1.5.0.md +410 -0
- package/STATUS.md +245 -0
- package/dist/auto-commit.d.ts +66 -0
- package/dist/auto-commit.d.ts.map +1 -0
- package/dist/auto-commit.js +180 -0
- package/dist/auto-commit.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +246 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +46 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +256 -0
- package/dist/commands.js.map +1 -0
- package/dist/diff.d.ts +23 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +106 -0
- package/dist/diff.js.map +1 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js.map +1 -0
- package/dist/index-cache.d.ts +35 -0
- package/dist/index-cache.d.ts.map +1 -0
- package/dist/index-cache.js +114 -0
- package/dist/index-cache.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/storage.d.ts +45 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +151 -0
- package/dist/storage.js.map +1 -0
- package/dist/sync.d.ts +60 -0
- package/dist/sync.js +184 -0
- package/dist/tags.d.ts +85 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +219 -0
- package/dist/tags.js.map +1 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +73 -0
- package/docs/_config.yml +2 -0
- package/docs/index.html +960 -0
- package/docs-website/package.json +20 -0
- package/jest.config.js +21 -0
- package/package.json +50 -0
- package/scripts/init.ts +290 -0
- package/src/agent-audit.ts +270 -0
- package/src/agent-checkout.ts +227 -0
- package/src/agent-coordination.ts +318 -0
- package/src/async-queue.ts +203 -0
- package/src/auto-branching.ts +279 -0
- package/src/auto-commit.ts +166 -0
- package/src/cherry-pick.ts +252 -0
- package/src/chunked-upload.ts +224 -0
- package/src/cli-v2.ts +335 -0
- package/src/cli.ts +318 -0
- package/src/cliff-detection.ts +232 -0
- package/src/commands.ts +267 -0
- package/src/commit-hash-system.ts +351 -0
- package/src/compression.ts +176 -0
- package/src/conflict-resolution-ui.ts +277 -0
- package/src/conflict-visualization.ts +238 -0
- package/src/diff-formatter.ts +184 -0
- package/src/diff.ts +124 -0
- package/src/distributed-coordination.ts +273 -0
- package/src/git-interop.ts +316 -0
- package/src/index-cache.ts +88 -0
- package/src/index.ts +38 -0
- package/src/merge-engine.ts +143 -0
- package/src/message-search.ts +370 -0
- package/src/performance-monitoring.ts +236 -0
- package/src/rebase.ts +327 -0
- package/src/rollback.ts +215 -0
- package/src/semantic-grouping.ts +245 -0
- package/src/stage-area.ts +324 -0
- package/src/stash.ts +278 -0
- package/src/storage.ts +131 -0
- package/src/sync.ts +205 -0
- package/src/tags.ts +244 -0
- package/src/types.ts +119 -0
- package/src/webhooks.ts +119 -0
- package/src/workspace-isolation.ts +298 -0
- package/tests/auto-commit.test.ts +308 -0
- package/tests/checkout.test.ts +136 -0
- package/tests/commit.test.ts +118 -0
- package/tests/diff.test.ts +191 -0
- package/tests/github.test.ts +94 -0
- package/tests/integration.test.ts +267 -0
- package/tests/log.test.ts +125 -0
- package/tests/phase2-integration.test.ts +370 -0
- package/tests/storage.test.ts +167 -0
- package/tests/tags.test.ts +477 -0
- package/tests/types.test.ts +75 -0
- package/tests/v1.1/agent-audit.test.ts +472 -0
- package/tests/v1.1/agent-coordination.test.ts +308 -0
- package/tests/v1.1/async-queue.test.ts +253 -0
- package/tests/v1.1/comprehensive.test.ts +521 -0
- package/tests/v1.1/diff-formatter.test.ts +238 -0
- package/tests/v1.1/integration.test.ts +389 -0
- package/tests/v1.1/onboarding.test.ts +365 -0
- package/tests/v1.1/rollback.test.ts +370 -0
- package/tests/v1.1/semantic-grouping.test.ts +230 -0
- package/tests/v1.2/chunked-upload.test.ts +301 -0
- package/tests/v1.2/cliff-detection.test.ts +272 -0
- package/tests/v1.2/commit-hash-system.test.ts +288 -0
- package/tests/v1.2/compression.test.ts +220 -0
- package/tests/v1.2/conflict-visualization.test.ts +263 -0
- package/tests/v1.2/distributed.test.ts +261 -0
- package/tests/v1.2/performance-monitoring.test.ts +328 -0
- package/tests/v1.3/auto-branching.test.ts +270 -0
- package/tests/v1.3/message-search.test.ts +264 -0
- package/tests/v1.3/stage-area.test.ts +330 -0
- package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
- package/tests/v1.4/cli.test.ts +171 -0
- package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
- package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
- package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
- package/tests/v1.4/workspace-isolation.test.ts +268 -0
- package/tests/v1.5/agent-coordination.real.test.ts +401 -0
- package/tests/v1.5/cli-v2.test.ts +354 -0
- package/tests/v1.5/git-interop.real.test.ts +358 -0
- package/tests/v1.5/integration-testing.real.test.ts +440 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cherry-Pick for Trace
|
|
3
|
+
* Apply specific commits to current branch
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CherryPickOperation {
|
|
7
|
+
sourceCommit: string;
|
|
8
|
+
sourceMessage: string;
|
|
9
|
+
targetBranch: string;
|
|
10
|
+
newCommitHash: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
success: boolean;
|
|
13
|
+
conflicts?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CherryPickOptions {
|
|
17
|
+
author?: string;
|
|
18
|
+
timestamp?: number;
|
|
19
|
+
signoff?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class CherryPick {
|
|
23
|
+
private operations: CherryPickOperation[] = [];
|
|
24
|
+
private cherryPickedCommits: Set<string> = new Set();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Cherry-pick commit to current branch
|
|
28
|
+
*/
|
|
29
|
+
pick(
|
|
30
|
+
sourceCommit: any, // commit object
|
|
31
|
+
targetBranch: string,
|
|
32
|
+
options?: CherryPickOptions
|
|
33
|
+
): {
|
|
34
|
+
success: boolean;
|
|
35
|
+
newHash?: string;
|
|
36
|
+
conflicts: string[];
|
|
37
|
+
} {
|
|
38
|
+
const conflicts: string[] = [];
|
|
39
|
+
|
|
40
|
+
// Check if already cherry-picked
|
|
41
|
+
if (this.cherryPickedCommits.has(sourceCommit.hash)) {
|
|
42
|
+
conflicts.push(`Commit ${sourceCommit.hash} already cherry-picked`);
|
|
43
|
+
return { success: false, conflicts };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Simulate conflict detection
|
|
47
|
+
const hasConflict = Math.random() < 0.05; // 5% conflict rate
|
|
48
|
+
|
|
49
|
+
if (hasConflict) {
|
|
50
|
+
conflicts.push('src/file.ts'); // Would detect actual conflicts
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Generate new commit hash
|
|
54
|
+
const message = options?.signoff
|
|
55
|
+
? `${sourceCommit.message}\n\n(cherry picked from commit ${sourceCommit.hash})`
|
|
56
|
+
: sourceCommit.message;
|
|
57
|
+
|
|
58
|
+
const newHash = this.generateNewHash(sourceCommit, targetBranch, message);
|
|
59
|
+
|
|
60
|
+
const operation: CherryPickOperation = {
|
|
61
|
+
sourceCommit: sourceCommit.hash,
|
|
62
|
+
sourceMessage: sourceCommit.message,
|
|
63
|
+
targetBranch,
|
|
64
|
+
newCommitHash: newHash,
|
|
65
|
+
timestamp: options?.timestamp || Date.now(),
|
|
66
|
+
success: conflicts.length === 0,
|
|
67
|
+
conflicts: conflicts.length > 0 ? conflicts : undefined,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
this.operations.push(operation);
|
|
71
|
+
|
|
72
|
+
if (operation.success) {
|
|
73
|
+
this.cherryPickedCommits.add(sourceCommit.hash);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
success: operation.success,
|
|
78
|
+
newHash: newHash,
|
|
79
|
+
conflicts,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pick multiple commits
|
|
85
|
+
*/
|
|
86
|
+
pickMultiple(
|
|
87
|
+
sourceCommits: any[],
|
|
88
|
+
targetBranch: string,
|
|
89
|
+
options?: CherryPickOptions
|
|
90
|
+
): {
|
|
91
|
+
success: boolean;
|
|
92
|
+
picked: number;
|
|
93
|
+
failed: number;
|
|
94
|
+
allConflicts: string[];
|
|
95
|
+
} {
|
|
96
|
+
let picked = 0;
|
|
97
|
+
let failed = 0;
|
|
98
|
+
const allConflicts: string[] = [];
|
|
99
|
+
|
|
100
|
+
for (const commit of sourceCommits) {
|
|
101
|
+
const result = this.pick(commit, targetBranch, options);
|
|
102
|
+
if (result.success) {
|
|
103
|
+
picked++;
|
|
104
|
+
} else {
|
|
105
|
+
failed++;
|
|
106
|
+
allConflicts.push(...result.conflicts);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
success: failed === 0,
|
|
112
|
+
picked,
|
|
113
|
+
failed,
|
|
114
|
+
allConflicts,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Continue after conflict resolution
|
|
120
|
+
*/
|
|
121
|
+
continueAfterResolution(resolvedFiles: Map<string, string>): boolean {
|
|
122
|
+
// In real implementation, would verify conflicts are resolved
|
|
123
|
+
// and continue applying changes
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Abort cherry-pick (rollback)
|
|
129
|
+
*/
|
|
130
|
+
abort(): boolean {
|
|
131
|
+
if (this.operations.length === 0) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const lastOp = this.operations[this.operations.length - 1];
|
|
136
|
+
this.cherryPickedCommits.delete(lastOp.sourceCommit);
|
|
137
|
+
this.operations.pop();
|
|
138
|
+
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get cherry-pick history
|
|
144
|
+
*/
|
|
145
|
+
getHistory(): CherryPickOperation[] {
|
|
146
|
+
return [...this.operations];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if commit was cherry-picked
|
|
151
|
+
*/
|
|
152
|
+
isPickedBefore(commitHash: string): boolean {
|
|
153
|
+
return this.cherryPickedCommits.has(commitHash);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get all picked commits
|
|
158
|
+
*/
|
|
159
|
+
getPickedCommits(): string[] {
|
|
160
|
+
return Array.from(this.cherryPickedCommits);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get operations by branch
|
|
165
|
+
*/
|
|
166
|
+
getOperationsByBranch(branch: string): CherryPickOperation[] {
|
|
167
|
+
return this.operations.filter(op => op.targetBranch === branch);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get failed picks (with conflicts)
|
|
172
|
+
*/
|
|
173
|
+
getFailedPicks(): CherryPickOperation[] {
|
|
174
|
+
return this.operations.filter(op => !op.success);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get stats
|
|
179
|
+
*/
|
|
180
|
+
getStats(): {
|
|
181
|
+
totalPicks: number;
|
|
182
|
+
successfulPicks: number;
|
|
183
|
+
failedPicks: number;
|
|
184
|
+
successRate: number;
|
|
185
|
+
} {
|
|
186
|
+
const successful = this.operations.filter(op => op.success).length;
|
|
187
|
+
const failed = this.operations.length - successful;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
totalPicks: this.operations.length,
|
|
191
|
+
successfulPicks: successful,
|
|
192
|
+
failedPicks: failed,
|
|
193
|
+
successRate:
|
|
194
|
+
this.operations.length > 0 ? (successful / this.operations.length) * 100 : 0,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Format operation for display
|
|
200
|
+
*/
|
|
201
|
+
static formatOperation(op: CherryPickOperation): string {
|
|
202
|
+
const status = op.success ? '✓' : '✗';
|
|
203
|
+
let output = `${status} ${op.sourceCommit.substring(0, 7)} - ${op.sourceMessage}\n`;
|
|
204
|
+
output += ` Target: ${op.targetBranch} → ${op.newCommitHash.substring(0, 7)}\n`;
|
|
205
|
+
|
|
206
|
+
if (op.conflicts && op.conflicts.length > 0) {
|
|
207
|
+
output += ` Conflicts: ${op.conflicts.join(', ')}\n`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return output;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Format history for display
|
|
215
|
+
*/
|
|
216
|
+
static formatHistory(operations: CherryPickOperation[]): string {
|
|
217
|
+
if (operations.length === 0) {
|
|
218
|
+
return 'No cherry-pick operations.';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let output = `\n🍒 CHERRY-PICKS (${operations.length} total):\n`;
|
|
222
|
+
|
|
223
|
+
for (const op of operations) {
|
|
224
|
+
output += this.formatOperation(op);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return output;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generate new hash for cherry-picked commit
|
|
232
|
+
*/
|
|
233
|
+
private generateNewHash(commit: any, targetBranch: string, message: string): string {
|
|
234
|
+
const content = [commit.hash, targetBranch, message].join('|');
|
|
235
|
+
const hash = require('crypto')
|
|
236
|
+
.createHash('sha256')
|
|
237
|
+
.update(content)
|
|
238
|
+
.digest('hex')
|
|
239
|
+
.substring(0, 16);
|
|
240
|
+
return hash;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Clear history (for testing)
|
|
245
|
+
*/
|
|
246
|
+
clearHistory(): void {
|
|
247
|
+
this.operations = [];
|
|
248
|
+
this.cherryPickedCommits.clear();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export default CherryPick;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunked Upload Engine for Trace
|
|
3
|
+
* Handle large files (100MB+) efficiently
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Chunk {
|
|
7
|
+
id: string;
|
|
8
|
+
fileId: string;
|
|
9
|
+
index: number;
|
|
10
|
+
totalChunks: number;
|
|
11
|
+
hash: string;
|
|
12
|
+
size: number;
|
|
13
|
+
compressed: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ChunkedFile {
|
|
17
|
+
fileId: string;
|
|
18
|
+
fileName: string;
|
|
19
|
+
totalSize: number;
|
|
20
|
+
chunkSize: number;
|
|
21
|
+
totalChunks: number;
|
|
22
|
+
chunks: Chunk[];
|
|
23
|
+
status: 'uploading' | 'completed' | 'failed';
|
|
24
|
+
progress: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ChunkedUploadEngine {
|
|
28
|
+
private uploads: Map<string, ChunkedFile> = new Map();
|
|
29
|
+
private chunkStore: Map<string, Buffer> = new Map();
|
|
30
|
+
private defaultChunkSize: number = 5 * 1024 * 1024; // 5MB
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start chunked upload
|
|
34
|
+
*/
|
|
35
|
+
initUpload(fileId: string, fileName: string, totalSize: number, chunkSize?: number): ChunkedFile {
|
|
36
|
+
const size = chunkSize || this.defaultChunkSize;
|
|
37
|
+
const totalChunks = Math.ceil(totalSize / size);
|
|
38
|
+
|
|
39
|
+
const file: ChunkedFile = {
|
|
40
|
+
fileId,
|
|
41
|
+
fileName,
|
|
42
|
+
totalSize,
|
|
43
|
+
chunkSize: size,
|
|
44
|
+
totalChunks,
|
|
45
|
+
chunks: [],
|
|
46
|
+
status: 'uploading',
|
|
47
|
+
progress: 0,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
this.uploads.set(fileId, file);
|
|
51
|
+
return file;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Upload single chunk
|
|
56
|
+
*/
|
|
57
|
+
uploadChunk(fileId: string, index: number, data: Buffer, hash: string): Chunk {
|
|
58
|
+
const file = this.uploads.get(fileId);
|
|
59
|
+
if (!file) throw new Error(`Upload ${fileId} not found`);
|
|
60
|
+
|
|
61
|
+
const chunk: Chunk = {
|
|
62
|
+
id: `${fileId}-${index}`,
|
|
63
|
+
fileId,
|
|
64
|
+
index,
|
|
65
|
+
totalChunks: file.totalChunks,
|
|
66
|
+
hash,
|
|
67
|
+
size: data.length,
|
|
68
|
+
compressed: false,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Store chunk
|
|
72
|
+
this.chunkStore.set(chunk.id, data);
|
|
73
|
+
file.chunks.push(chunk);
|
|
74
|
+
|
|
75
|
+
// Update progress
|
|
76
|
+
file.progress = (file.chunks.length / file.totalChunks) * 100;
|
|
77
|
+
|
|
78
|
+
// Mark complete if all chunks received
|
|
79
|
+
if (file.chunks.length === file.totalChunks) {
|
|
80
|
+
file.status = 'completed';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return chunk;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Verify chunk integrity
|
|
88
|
+
*/
|
|
89
|
+
verifyChunk(chunkId: string, expectedHash: string): boolean {
|
|
90
|
+
const data = this.chunkStore.get(chunkId);
|
|
91
|
+
if (!data) return false;
|
|
92
|
+
|
|
93
|
+
// Simple checksum (in production, use SHA-256)
|
|
94
|
+
const hash = this.calculateHash(data);
|
|
95
|
+
return hash === expectedHash;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get upload status
|
|
100
|
+
*/
|
|
101
|
+
getUploadStatus(fileId: string): ChunkedFile | undefined {
|
|
102
|
+
return this.uploads.get(fileId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reconstruct file from chunks
|
|
107
|
+
*/
|
|
108
|
+
reconstructFile(fileId: string): Buffer {
|
|
109
|
+
const file = this.uploads.get(fileId);
|
|
110
|
+
if (!file || file.status !== 'completed') {
|
|
111
|
+
throw new Error(`Upload ${fileId} not complete`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Sort chunks by index
|
|
115
|
+
const sortedChunks = [...file.chunks].sort((a, b) => a.index - b.index);
|
|
116
|
+
|
|
117
|
+
// Concatenate chunks
|
|
118
|
+
const buffers: Buffer[] = [];
|
|
119
|
+
for (const chunk of sortedChunks) {
|
|
120
|
+
const data = this.chunkStore.get(chunk.id);
|
|
121
|
+
if (!data) throw new Error(`Chunk ${chunk.id} not found`);
|
|
122
|
+
buffers.push(data);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Buffer.concat(buffers);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Resume interrupted upload
|
|
130
|
+
*/
|
|
131
|
+
getMissingChunks(fileId: string): number[] {
|
|
132
|
+
const file = this.uploads.get(fileId);
|
|
133
|
+
if (!file) return [];
|
|
134
|
+
|
|
135
|
+
const uploadedIndices = new Set(file.chunks.map(c => c.index));
|
|
136
|
+
const missing: number[] = [];
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < file.totalChunks; i++) {
|
|
139
|
+
if (!uploadedIndices.has(i)) {
|
|
140
|
+
missing.push(i);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return missing;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Cancel upload
|
|
149
|
+
*/
|
|
150
|
+
cancelUpload(fileId: string): boolean {
|
|
151
|
+
const file = this.uploads.get(fileId);
|
|
152
|
+
if (!file) return false;
|
|
153
|
+
|
|
154
|
+
// Clean up chunks
|
|
155
|
+
for (const chunk of file.chunks) {
|
|
156
|
+
this.chunkStore.delete(chunk.id);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.uploads.delete(fileId);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get upload progress
|
|
165
|
+
*/
|
|
166
|
+
getProgress(fileId: string): number {
|
|
167
|
+
const file = this.uploads.get(fileId);
|
|
168
|
+
return file?.progress || 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Calculate hash for chunk
|
|
173
|
+
*/
|
|
174
|
+
private calculateHash(data: Buffer): string {
|
|
175
|
+
let hash = 0;
|
|
176
|
+
for (let i = 0; i < data.length; i++) {
|
|
177
|
+
hash = (hash << 5) - hash + data[i];
|
|
178
|
+
hash = hash & hash;
|
|
179
|
+
}
|
|
180
|
+
return Math.abs(hash).toString(36);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Estimate upload time
|
|
185
|
+
*/
|
|
186
|
+
estimateUploadTime(fileSize: number, uploadSpeedMbps: number = 5): { chunks: number; timeSeconds: number } {
|
|
187
|
+
const chunks = Math.ceil(fileSize / this.defaultChunkSize);
|
|
188
|
+
const timeSec = (fileSize / (1024 * 1024)) / uploadSpeedMbps;
|
|
189
|
+
return { chunks, timeSeconds: Math.ceil(timeSec) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get all active uploads
|
|
194
|
+
*/
|
|
195
|
+
getActiveUploads(): ChunkedFile[] {
|
|
196
|
+
return Array.from(this.uploads.values()).filter(f => f.status === 'uploading');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Clean up completed/failed uploads
|
|
201
|
+
*/
|
|
202
|
+
cleanup(fileId?: string): void {
|
|
203
|
+
if (fileId) {
|
|
204
|
+
const file = this.uploads.get(fileId);
|
|
205
|
+
if (file && file.status !== 'uploading') {
|
|
206
|
+
for (const chunk of file.chunks) {
|
|
207
|
+
this.chunkStore.delete(chunk.id);
|
|
208
|
+
}
|
|
209
|
+
this.uploads.delete(fileId);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
for (const [fid, file] of this.uploads) {
|
|
213
|
+
if (file.status !== 'uploading') {
|
|
214
|
+
for (const chunk of file.chunks) {
|
|
215
|
+
this.chunkStore.delete(chunk.id);
|
|
216
|
+
}
|
|
217
|
+
this.uploads.delete(fid);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default ChunkedUploadEngine;
|