amule-ec-client 0.7.0 → 0.7.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.
Files changed (40) hide show
  1. package/dist/{AddLinkRequest-DEKGy-R2.js → AddLinkRequest-CGv1H3PC.js} +2 -2
  2. package/dist/{ClientQueueRequest-DrLXSxtp.js → ClientQueueRequest-mklt-KLi.js} +2 -2
  3. package/dist/{ClientQueueResponse-CXxY_9wU.js → ClientQueueResponse-RfXh2JGa.js} +3 -2
  4. package/dist/{CreateCategoryRequest-COulF12x.js → CreateCategoryRequest-Cyp9G7jU.js} +2 -2
  5. package/dist/{DeleteCategoryRequest-D-mljaNV.js → DeleteCategoryRequest-DbLJ4mvi.js} +2 -2
  6. package/dist/{DownloadCommandRequest-Bf1Ez8Da.js → DownloadCommandRequest-DLU7RtYs.js} +2 -2
  7. package/dist/{DownloadFileRequest-WfOIV07R.js → DownloadFileRequest-Db7oijyu.js} +2 -2
  8. package/dist/{DownloadQueueRequest-Br-pgrRt.js → DownloadQueueRequest-DdHRjl4L.js} +2 -2
  9. package/dist/{GetPreferencesRequest-BE3RbRRn.js → GetPreferencesRequest-BlWGYNCP.js} +2 -2
  10. package/dist/{PreferencesDetailsResponse-D8lBd8Ot.js → PreferencesDetailsResponse-BX6qUoga.js} +1 -1
  11. package/dist/{PreferencesResponse-BmGTQM30.js → PreferencesResponse-migFtIG7.js} +1 -1
  12. package/dist/{ReloadSharedFilesRequest-BbDUeGcq.js → ReloadSharedFilesRequest-WD9iYLje.js} +2 -2
  13. package/dist/{Request-BvScDpeu.js → Request-YQ0nIM6b.js} +1 -1
  14. package/dist/{SearchProgressResponse-BQPliUup.js → SearchProgressResponse-CkEgZGFE.js} +1 -1
  15. package/dist/{SearchRequest-DOakj8fA.js → SearchRequest-DaAKS4IO.js} +2 -2
  16. package/dist/{SearchResultsRequest-oZBs5KKm.js → SearchResultsRequest-B53jwjRH.js} +2 -2
  17. package/dist/{SearchResultsResponse-C9I9x035.js → SearchResultsResponse-C1l-bBja.js} +1 -1
  18. package/dist/{SearchStatusRequest-B1ZzSQlE.js → SearchStatusRequest-DuEa5ZYu.js} +2 -2
  19. package/dist/{SearchStopRequest-FfXf_DCn.js → SearchStopRequest-B5TZdMiB.js} +2 -2
  20. package/dist/{ServerConnectRequest-Cdiw-KWu.js → ServerConnectRequest-BeB8RSBN.js} +2 -2
  21. package/dist/{ServerDisconnectRequest-BVXSumxX.js → ServerDisconnectRequest-CSkUHETP.js} +2 -2
  22. package/dist/{ServerListRequest-3yMrgZID.js → ServerListRequest-B82686jD.js} +2 -2
  23. package/dist/{ServerListResponse-Do3iRai0.js → ServerListResponse-M3C2OcfZ.js} +1 -1
  24. package/dist/{ServerUpdateFromUrlRequest-DpxBII5W.js → ServerUpdateFromUrlRequest-BcvhRKgH.js} +2 -2
  25. package/dist/{SetFileCategoryRequest-CFgkJB-H.js → SetFileCategoryRequest-C1jWsYO5.js} +2 -2
  26. package/dist/{SetPreferencesRequest-BdZ40Ao_.js → SetPreferencesRequest-CNJVmeQl.js} +2 -2
  27. package/dist/{SharedFilePriorityRequest-BMNmaL06.js → SharedFilePriorityRequest-CBqkaXaK.js} +2 -2
  28. package/dist/{SharedFilesRequest-BZKz0oyn.js → SharedFilesRequest-5mKKjzgt.js} +2 -2
  29. package/dist/{SharedFilesResponse-DE0cd1DT.js → SharedFilesResponse-CFl0NuO-.js} +1 -1
  30. package/dist/{StatsRequest-DVwx5wk1.js → StatsRequest-C9gBUvSq.js} +2 -2
  31. package/dist/{StatsResponse-CP2F2zt0.js → StatsResponse-KOk-xT6_.js} +1 -1
  32. package/dist/{Tag-B5gWV2XF.js → Tag-D_Ybe5RQ.js} +17 -5
  33. package/dist/{UpdateCategoryRequest-Bu22YjOm.js → UpdateCategoryRequest-BOYfb6Vn.js} +2 -2
  34. package/dist/{UpdateRequest-CaQ867p8.js → UpdateRequest-csLQz7JO.js} +2 -2
  35. package/dist/UpdateState-LhdtiATk.js +443 -0
  36. package/dist/index.d.ts +24 -2
  37. package/dist/index.js +63 -44
  38. package/package.json +1 -1
  39. package/dist/DownloadQueueResponse-C-5d4teB.js +0 -241
  40. package/dist/UpdateState-Dn-SuAsj.js +0 -173
@@ -0,0 +1,443 @@
1
+ import { f as findNumericTag, p as findTag } from "./Tag-D_Ybe5RQ.js";
2
+ import { n as PARTSIZE, t as ChunkStatus } from "./download-details-OM0FJUhZ.js";
3
+ import { a as toOptionalNumber, i as toOptionalIp, n as tagOwnNumericValue, r as toOptionalBool } from "./utils-CNoUroOI.js";
4
+ import { r as parseKnownFileFields, t as SharedFilesResponseParser } from "./SharedFilesResponse-CFl0NuO-.js";
5
+ import { t as ClientQueueResponseParser } from "./ClientQueueResponse-RfXh2JGa.js";
6
+ import { t as ServerListResponseParser } from "./ServerListResponse-M3C2OcfZ.js";
7
+ //#region src/response/DownloadDetailsResponse.ts
8
+ /**
9
+ * Decode an aMule EC RLE-compressed buffer.
10
+ * Format: [value, value, count] repeats value count times; other bytes pass through.
11
+ */
12
+ function rleDecode(buf) {
13
+ let size = 0;
14
+ let index = 0;
15
+ while (index < buf.length) if (index < buf.length - 2 && buf[index + 1] === buf[index]) {
16
+ size += buf[index + 2];
17
+ index += 3;
18
+ } else {
19
+ size += 1;
20
+ index += 1;
21
+ }
22
+ const output = Buffer.alloc(size);
23
+ let outIndex = 0;
24
+ index = 0;
25
+ while (index < buf.length) if (index < buf.length - 2 && buf[index + 1] === buf[index]) {
26
+ const count = buf[index + 2];
27
+ output.fill(buf[index], outIndex, outIndex + count);
28
+ outIndex += count;
29
+ index += 3;
30
+ } else {
31
+ output[outIndex++] = buf[index];
32
+ index += 1;
33
+ }
34
+ return output;
35
+ }
36
+ /**
37
+ * Reconstruct the current state from the previous state and an RLE-decoded XOR diff,
38
+ * mirroring aMule's RLE_Data: resize previous buffer to the diff size (zero-extend on
39
+ * grow, truncate on shrink), then XOR byte-wise.
40
+ */
41
+ function xorReconstruct(previous, decodedDiff) {
42
+ const result = Buffer.alloc(decodedDiff.length, 0);
43
+ previous.copy(result, 0, 0, Math.min(previous.length, decodedDiff.length));
44
+ for (let i = 0; i < decodedDiff.length; i++) result[i] ^= decodedDiff[i];
45
+ return result;
46
+ }
47
+ /**
48
+ * Decode an RLE-decoded buffer of byte-interleaved (column-major) little-endian uint64 values.
49
+ */
50
+ function decodeInterleavedUint64(decoded) {
51
+ if (decoded.length === 0 || decoded.length % 8 !== 0) return [];
52
+ const valueCount = decoded.length / 8;
53
+ const values = new Array(valueCount);
54
+ for (let i = 0; i < valueCount; i++) {
55
+ let value = 0;
56
+ for (let byte = 7; byte >= 0; byte--) value = value * 256 + decoded[i + byte * valueCount];
57
+ values[i] = value;
58
+ }
59
+ return values;
60
+ }
61
+ function stringValue(tag) {
62
+ if (!tag) return "";
63
+ const value = tag.getValue();
64
+ return typeof value === "string" ? value : "";
65
+ }
66
+ function bufferValue(tag) {
67
+ if (!tag) return;
68
+ const value = tag.getValue();
69
+ return Buffer.isBuffer(value) ? value : void 0;
70
+ }
71
+ function nestedTag(parent, tagName) {
72
+ return parent.nestedTags ? findTag(parent.nestedTags, tagName) : void 0;
73
+ }
74
+ function nestedNumeric(parent, tagName) {
75
+ if (!parent.nestedTags) return 0;
76
+ const numeric = findNumericTag(parent.nestedTags, tagName);
77
+ if (numeric) return Number(numeric.getValue());
78
+ return tagOwnNumericValue(findTag(parent.nestedTags, tagName)) ?? 0;
79
+ }
80
+ /**
81
+ * Extract the raw (RLE-encoded) part/gap/req status buffers of a partfile tag.
82
+ */
83
+ function extractPartFileStatusBuffers(fileTag) {
84
+ return {
85
+ partStatus: bufferValue(nestedTag(fileTag, 786)),
86
+ gapStatus: bufferValue(nestedTag(fileTag, 787)),
87
+ reqStatus: bufferValue(nestedTag(fileTag, 788))
88
+ };
89
+ }
90
+ /**
91
+ * Compute per-chunk status from RLE-decoded part/gap/req status buffers.
92
+ */
93
+ function computeChunkInfo(sizeFull, buffers) {
94
+ const partCount = sizeFull > 0 ? Math.ceil(sizeFull / PARTSIZE) : 0;
95
+ const availability = new Array(partCount).fill(0);
96
+ if (buffers.partStatus && buffers.partStatus.length > 0) for (let i = 0; i < Math.min(buffers.partStatus.length, partCount); i++) availability[i] = buffers.partStatus[i];
97
+ const chunks = new Array(partCount).fill(ChunkStatus.COMPLETE);
98
+ if (partCount > 0 && buffers.gapStatus && buffers.gapStatus.length > 0) {
99
+ const gapValues = decodeInterleavedUint64(buffers.gapStatus);
100
+ for (let i = 0; i + 1 < gapValues.length; i += 2) {
101
+ const gapStart = gapValues[i];
102
+ const gapEnd = gapValues[i + 1];
103
+ if (gapStart > sizeFull || gapEnd > sizeFull || gapStart >= gapEnd) continue;
104
+ const startPart = Math.floor(gapStart / PARTSIZE);
105
+ const endPart = Math.min(Math.floor(gapEnd / PARTSIZE) + 1, partCount);
106
+ for (let part = startPart; part < endPart; part++) {
107
+ const chunkStart = part * PARTSIZE;
108
+ if (gapStart < Math.min((part + 1) * 9728e3, sizeFull) && gapEnd > chunkStart) chunks[part] = availability[part] > 0 ? ChunkStatus.AVAILABLE : ChunkStatus.UNAVAILABLE;
109
+ }
110
+ }
111
+ }
112
+ if (partCount > 0 && buffers.reqStatus && buffers.reqStatus.length > 0) {
113
+ const reqValues = decodeInterleavedUint64(buffers.reqStatus);
114
+ for (let i = 0; i + 1 < reqValues.length; i += 2) {
115
+ const reqStart = reqValues[i];
116
+ const reqEnd = reqValues[i + 1];
117
+ if (reqStart > sizeFull || reqEnd > sizeFull || reqStart >= reqEnd) continue;
118
+ const startPart = Math.floor(reqStart / PARTSIZE);
119
+ const endPart = Math.min(Math.floor(reqEnd / PARTSIZE) + 1, partCount);
120
+ for (let part = startPart; part < endPart; part++) {
121
+ if (chunks[part] === ChunkStatus.COMPLETE) continue;
122
+ const chunkStart = part * PARTSIZE;
123
+ if (reqStart < Math.min((part + 1) * 9728e3, sizeFull) && reqEnd > chunkStart) chunks[part] = ChunkStatus.DOWNLOADING;
124
+ }
125
+ }
126
+ }
127
+ return {
128
+ chunks,
129
+ availability,
130
+ partCount,
131
+ sizeFull
132
+ };
133
+ }
134
+ /**
135
+ * Decode the status buffers of a file tag encoded after a daemon-side encoder reset,
136
+ * i.e. a full snapshot: EC_OP_GET_DLOAD_QUEUE / EC_OP_GET_SHARED_FILES responses at any
137
+ * detail level other than EC_DETAIL_UPDATE (see Get_EC_Response_GetDownloadQueue in
138
+ * aMule's ExternalConn.cpp). After the reset, a present buffer holds the complete
139
+ * current state and an absent one means the state is empty (the daemon skips the tag
140
+ * when there is nothing to encode), so every key is always defined.
141
+ */
142
+ function decodeFullPartFileStatusBuffers(fileTag) {
143
+ const raw = extractPartFileStatusBuffers(fileTag);
144
+ return {
145
+ partStatus: raw.partStatus ? rleDecode(raw.partStatus) : Buffer.alloc(0),
146
+ gapStatus: raw.gapStatus ? rleDecode(raw.gapStatus) : Buffer.alloc(0),
147
+ reqStatus: raw.reqStatus ? rleDecode(raw.reqStatus) : Buffer.alloc(0)
148
+ };
149
+ }
150
+ /**
151
+ * Raw diff entries of the source-names map of a partfile tag, or undefined when the
152
+ * packet carries no source-names changes for this file.
153
+ *
154
+ * The daemon sends source names (the filenames under which the sources share the file)
155
+ * as a per-connection incremental map (see CPartFile_Encoder::Encode in aMule's
156
+ * ExternalConn.cpp): an EC_TAG_PARTFILE_SOURCE_NAMES container whose children are
157
+ * integer-valued EC_TAG_PARTFILE_SOURCE_NAMES tags (the map key), each carrying a
158
+ * nested count (0 removes the entry) and, only when the entry is new, a nested name
159
+ * string. The map is never reset while the connection lives, not even for
160
+ * EC_DETAIL_FULL requests.
161
+ */
162
+ function sourceNameEntriesFromFileTag(fileTag) {
163
+ const container = nestedTag(fileTag, 789);
164
+ if (!container) return;
165
+ const entries = [];
166
+ for (const child of container.nestedTags || []) {
167
+ if (child.name !== 789) continue;
168
+ const id = tagOwnNumericValue(child);
169
+ if (id === void 0) continue;
170
+ const name = stringValue(nestedTag(child, 789)) || void 0;
171
+ const count = nestedNumeric(child, 796);
172
+ entries.push({
173
+ id,
174
+ name,
175
+ count
176
+ });
177
+ }
178
+ return entries;
179
+ }
180
+ //#endregion
181
+ //#region src/response/DownloadQueueResponse.ts
182
+ var DownloadQueueResponseParser = class {
183
+ static fromPacket(packet) {
184
+ const files = [];
185
+ const partfileTags = packet.tags.filter((tag) => tag.name === 768);
186
+ for (const fileTag of partfileTags) {
187
+ const tags = fileTag.nestedTags || [];
188
+ let a4afSources = void 0;
189
+ const a4afSourcesTag = findTag(tags, 802);
190
+ if (a4afSourcesTag) {
191
+ a4afSources = [];
192
+ a4afSourcesTag.nestedTags = a4afSourcesTag.nestedTags || [];
193
+ for (const sourceTag of a4afSourcesTag.nestedTags) if (sourceTag.name === 15) a4afSources.push(sourceTag.getValue());
194
+ }
195
+ const file = {
196
+ ...parseKnownFileFields(fileTag),
197
+ partMetID: findNumericTag(tags, 770)?.getShort(),
198
+ sizeXfer: toOptionalNumber(findNumericTag(tags, 772)?.getLong()),
199
+ sizeDone: toOptionalNumber(findNumericTag(tags, 774)?.getLong()),
200
+ fileStatus: findNumericTag(tags, 776)?.getInt(),
201
+ stopped: toOptionalBool(findNumericTag(tags, 791)?.getInt()),
202
+ sourceCount: findNumericTag(tags, 778)?.getShort(),
203
+ sourceNotCurrCount: findNumericTag(tags, 780)?.getShort(),
204
+ sourceXferCount: findNumericTag(tags, 781)?.getShort(),
205
+ sourceCountA4AF: findNumericTag(tags, 779)?.getShort(),
206
+ speed: toOptionalNumber(findNumericTag(tags, 775)?.getLong()),
207
+ downPrio: findNumericTag(tags, 777)?.getInt(),
208
+ fileCat: toOptionalNumber(findNumericTag(tags, 783)?.getLong()),
209
+ lastSeenComplete: toOptionalNumber(findNumericTag(tags, 785)?.getLong()),
210
+ lastDateChanged: toOptionalNumber(findNumericTag(tags, 784)?.getLong()),
211
+ downloadActiveTime: findNumericTag(tags, 792)?.getInt(),
212
+ availablePartCount: findNumericTag(tags, 797)?.getShort(),
213
+ a4AFAuto: toOptionalBool(findNumericTag(tags, 801)?.getInt()),
214
+ hashedPartCount: findNumericTag(tags, 800)?.getInt(),
215
+ getLostDueToCorruption: toOptionalNumber(findNumericTag(tags, 793)?.getLong()),
216
+ getGainDueToCompression: toOptionalNumber(findNumericTag(tags, 794)?.getLong()),
217
+ totalPacketsSavedDueToICH: findNumericTag(tags, 795)?.getInt(),
218
+ a4afSources
219
+ };
220
+ files.push(file);
221
+ }
222
+ return { files };
223
+ }
224
+ };
225
+ //#endregion
226
+ //#region src/response/UpdateResponse.ts
227
+ var UpdateResponseParser = class {
228
+ static fromPacket(packet) {
229
+ const sharedFiles = SharedFilesResponseParser.fromPacket(packet).files;
230
+ const downloadQueue = DownloadQueueResponseParser.fromPacket(packet).files;
231
+ const clientContainer = findTag(packet.tags, 1536);
232
+ const clients = clientContainer && clientContainer.nestedTags ? ClientQueueResponseParser.fromTags(clientContainer.nestedTags).clients : [];
233
+ const serverContainer = findTag(packet.tags, 1280);
234
+ const servers = serverContainer && serverContainer.nestedTags ? ServerListResponseParser.fromTags(serverContainer.nestedTags).servers : [];
235
+ const friendContainer = findTag(packet.tags, 2048);
236
+ return {
237
+ sharedFiles,
238
+ downloadQueue,
239
+ clients,
240
+ servers,
241
+ friends: friendContainer && friendContainer.nestedTags ? this.parseFriends(friendContainer.nestedTags) : []
242
+ };
243
+ }
244
+ static parseFriends(tags) {
245
+ const friends = [];
246
+ const friendTags = tags.filter((t) => t.name === 2048);
247
+ for (const friendTag of friendTags) {
248
+ const nested = friendTag.nestedTags || [];
249
+ friends.push({
250
+ name: findTag(nested, 2049)?.getValue(),
251
+ userHashHexString: findTag(nested, 2050)?.getValue()?.toString("hex"),
252
+ ip: toOptionalIp(findNumericTag(nested, 2051)?.getInt()),
253
+ port: findNumericTag(nested, 2052)?.getInt(),
254
+ friendSlot: toOptionalBool(findNumericTag(nested, 2056)?.getInt()),
255
+ shared: toOptionalBool(findNumericTag(nested, 2057)?.getInt()),
256
+ client: ClientQueueResponseParser.fromTags(nested).clients[0]
257
+ });
258
+ }
259
+ return friends;
260
+ }
261
+ };
262
+ //#endregion
263
+ //#region src/client/UpdateState.ts
264
+ const STATUS_BUFFER_KEYS = [
265
+ "partStatus",
266
+ "gapStatus",
267
+ "reqStatus"
268
+ ];
269
+ /**
270
+ * Merge update fields into the existing object. Fields absent from an incremental
271
+ * response are undefined after parsing and must not clobber previously known values.
272
+ */
273
+ function mergeDefined(existing, updates) {
274
+ const result = { ...existing };
275
+ for (const [key, value] of Object.entries(updates)) if (value !== void 0) result[key] = value;
276
+ return result;
277
+ }
278
+ var UpdateState = class {
279
+ sessionGeneration;
280
+ downloads = /* @__PURE__ */ new Map();
281
+ sharedFiles = /* @__PURE__ */ new Map();
282
+ clients = /* @__PURE__ */ new Map();
283
+ statusBuffers = /* @__PURE__ */ new Map();
284
+ sourceNames = /* @__PURE__ */ new Map();
285
+ /**
286
+ * @param sessionGeneration Connection generation this state belongs to (see
287
+ * AmuleConnection.getSessionGeneration). The state is only valid while the
288
+ * connection that produced it is alive, since the daemon diffs per connection.
289
+ */
290
+ constructor(sessionGeneration) {
291
+ this.sessionGeneration = sessionGeneration;
292
+ }
293
+ /**
294
+ * Apply an incremental EC_OP_GET_UPDATE response.
295
+ */
296
+ apply(packet) {
297
+ const parsed = UpdateResponseParser.fromPacket(packet);
298
+ const downloadQueue = this.mergeCollection(this.downloads, parsed.downloadQueue);
299
+ const sharedFiles = this.mergeCollection(this.sharedFiles, parsed.sharedFiles);
300
+ const clients = this.mergeCollection(this.clients, parsed.clients);
301
+ for (const ecid of [...this.statusBuffers.keys()]) if (!this.downloads.has(ecid)) this.statusBuffers.delete(ecid);
302
+ for (const ecid of [...this.sourceNames.keys()]) if (!this.downloads.has(ecid)) this.sourceNames.delete(ecid);
303
+ this.reconstructFileDetails(packet);
304
+ this.attachSources();
305
+ return {
306
+ downloadQueue,
307
+ sharedFiles,
308
+ clients,
309
+ servers: parsed.servers,
310
+ friends: parsed.friends
311
+ };
312
+ }
313
+ /**
314
+ * Apply a full (non-EC_DETAIL_UPDATE) EC_OP_GET_DLOAD_QUEUE response.
315
+ *
316
+ * The daemon resets each file's diff encoder before encoding it, so the buffers
317
+ * are complete snapshots that become the connection's new diff baseline; they are
318
+ * stored here as-is (replace, not XOR) to keep later incremental updates decodable.
319
+ * Source-name diffs are applied to the shared per-connection map.
320
+ */
321
+ applyDownloadQueue(packet) {
322
+ const parsed = DownloadQueueResponseParser.fromPacket(packet);
323
+ const partfileTags = packet.tags.filter((tag) => tag.name === 768);
324
+ const seen = /* @__PURE__ */ new Set();
325
+ for (const fileTag of partfileTags) {
326
+ const ecid = tagOwnNumericValue(fileTag);
327
+ if (ecid === void 0) continue;
328
+ seen.add(ecid);
329
+ this.statusBuffers.set(ecid, decodeFullPartFileStatusBuffers(fileTag));
330
+ this.applySourceNameEntries(ecid, sourceNameEntriesFromFileTag(fileTag));
331
+ }
332
+ for (const ecid of [...this.statusBuffers.keys()]) if (!seen.has(ecid)) this.statusBuffers.delete(ecid);
333
+ for (const ecid of [...this.sourceNames.keys()]) if (!seen.has(ecid)) this.sourceNames.delete(ecid);
334
+ for (const file of parsed.files) {
335
+ if (file.ecid === void 0) continue;
336
+ const buffers = this.statusBuffers.get(file.ecid);
337
+ if (buffers && file.sizeFull && file.sizeFull > 0) file.chunkInfo = computeChunkInfo(file.sizeFull, buffers);
338
+ const nameMap = this.sourceNames.get(file.ecid);
339
+ if (nameMap) file.sourceNames = [...nameMap.values()];
340
+ }
341
+ return parsed.files;
342
+ }
343
+ /**
344
+ * Track the encoder side effects of a full (non-EC_DETAIL_UPDATE)
345
+ * EC_OP_GET_SHARED_FILES response.
346
+ *
347
+ * Shared partfiles use the same per-connection encoder as the download paths: the
348
+ * daemon resets it and re-encodes part/gap/req status (and consumes source-name
349
+ * diffs) into each EC_TAG_KNOWNFILE tag, so the new baselines must be recorded here
350
+ * or the next incremental update decodes into garbage.
351
+ */
352
+ applySharedFiles(packet) {
353
+ const knownfileTags = packet.tags.filter((tag) => tag.name === 1024);
354
+ for (const fileTag of knownfileTags) {
355
+ const ecid = tagOwnNumericValue(fileTag);
356
+ if (ecid === void 0) continue;
357
+ this.statusBuffers.set(ecid, decodeFullPartFileStatusBuffers(fileTag));
358
+ this.applySourceNameEntries(ecid, sourceNameEntriesFromFileTag(fileTag));
359
+ }
360
+ }
361
+ mergeCollection(state, items) {
362
+ const seen = /* @__PURE__ */ new Set();
363
+ const result = [];
364
+ for (const item of items) {
365
+ if (item.ecid === void 0) {
366
+ result.push(item);
367
+ continue;
368
+ }
369
+ seen.add(item.ecid);
370
+ const existing = state.get(item.ecid);
371
+ const merged = existing ? mergeDefined(existing, item) : item;
372
+ state.set(item.ecid, merged);
373
+ result.push(merged);
374
+ }
375
+ for (const ecid of [...state.keys()]) if (!seen.has(ecid)) state.delete(ecid);
376
+ return result;
377
+ }
378
+ /**
379
+ * Reconstruct the incremental per-file details of each partfile in the packet:
380
+ * RLE-decode and XOR-reconstruct the status buffers (recomputing chunk info from the
381
+ * accumulated state), and apply the source-names map diffs.
382
+ */
383
+ reconstructFileDetails(packet) {
384
+ const partfileTags = packet.tags.filter((tag) => tag.name === 768);
385
+ for (const fileTag of partfileTags) {
386
+ const ecid = tagOwnNumericValue(fileTag);
387
+ if (ecid === void 0) continue;
388
+ const file = this.downloads.get(ecid);
389
+ if (!file) continue;
390
+ const raw = extractPartFileStatusBuffers(fileTag);
391
+ const state = this.statusBuffers.get(ecid) || {};
392
+ for (const key of STATUS_BUFFER_KEYS) {
393
+ const rawBuffer = raw[key];
394
+ if (!rawBuffer) continue;
395
+ const decoded = rleDecode(rawBuffer);
396
+ const previous = state[key];
397
+ state[key] = previous ? xorReconstruct(previous, decoded) : decoded;
398
+ }
399
+ this.statusBuffers.set(ecid, state);
400
+ if (file.sizeFull && file.sizeFull > 0) file.chunkInfo = computeChunkInfo(file.sizeFull, state);
401
+ this.applySourceNameEntries(ecid, sourceNameEntriesFromFileTag(fileTag));
402
+ const nameMap = this.sourceNames.get(ecid);
403
+ if (nameMap) file.sourceNames = [...nameMap.values()];
404
+ }
405
+ }
406
+ /**
407
+ * Apply source-names map diffs: new entry (carries the name), count change
408
+ * (no name resent) or removal (count 0).
409
+ */
410
+ applySourceNameEntries(ecid, entries) {
411
+ if (!entries) return;
412
+ const nameMap = this.sourceNames.get(ecid) || /* @__PURE__ */ new Map();
413
+ for (const entry of entries) {
414
+ if (entry.count === 0) {
415
+ nameMap.delete(entry.id);
416
+ continue;
417
+ }
418
+ const existing = nameMap.get(entry.id);
419
+ if (existing) {
420
+ existing.count = entry.count;
421
+ if (entry.name !== void 0) existing.name = entry.name;
422
+ } else if (entry.name !== void 0) nameMap.set(entry.id, {
423
+ name: entry.name,
424
+ count: entry.count
425
+ });
426
+ }
427
+ this.sourceNames.set(ecid, nameMap);
428
+ }
429
+ /**
430
+ * Group clients by the download they are requesting (client.requestFileId -> file.ecid).
431
+ */
432
+ attachSources() {
433
+ for (const file of this.downloads.values()) file.sources = [];
434
+ for (const client of this.clients.values()) {
435
+ const requestFileId = toOptionalNumber(client.requestFileId);
436
+ if (requestFileId === void 0) continue;
437
+ const file = this.downloads.get(requestFileId);
438
+ if (file) file.sources.push(client);
439
+ }
440
+ }
441
+ };
442
+ //#endregion
443
+ export { UpdateState };
package/dist/index.d.ts CHANGED
@@ -125,7 +125,9 @@ interface AmuleUpDownClient {
125
125
  serverIP?: string;
126
126
  serverPort?: number;
127
127
  serverName?: string;
128
+ /** Upload speed towards this client, in bytes/s */
128
129
  upSpeed?: number;
130
+ /** Download speed from this client, in bytes/s (the daemon sends kB/s; normalized on parse) */
129
131
  downSpeed?: number;
130
132
  uploadSession?: number;
131
133
  transferredDown?: number;
@@ -824,11 +826,18 @@ declare class StringTag extends Tag<string> {
824
826
  parseValue(value: Buffer): void;
825
827
  encodeValue(): Buffer;
826
828
  }
827
- declare class DoubleTag extends Tag<number> {
829
+ /**
830
+ * EC_TAGTYPE_DOUBLE tags are transmitted as a null-terminated ASCII decimal string,
831
+ * not as a binary double (see CECTag(ec_tagname_t, double) in aMule's ECTag.cpp).
832
+ */
833
+ declare class DoubleTag extends Tag<number> implements NumericTag {
828
834
  constructor(name: ECTagName, value?: number, subtags?: Tag<any>[], nameValue?: number);
829
835
  static withValue(name: ECTagName, value: number, subtags?: Tag<any>[]): DoubleTag;
830
836
  parseValue(value: Buffer): void;
831
837
  encodeValue(): Buffer;
838
+ getShort(): number;
839
+ getInt(): number;
840
+ getLong(): bigint;
832
841
  }
833
842
  interface Ipv4Value {
834
843
  address: string;
@@ -957,6 +966,14 @@ declare class AmuleClient {
957
966
  * Get server statistics
958
967
  */
959
968
  getStats(): Promise<StatsResponse>;
969
+ /**
970
+ * Per-connection file-detail state (diff baselines, source names, merged snapshots).
971
+ * The daemon diffs per connection and shares the diff state between the update,
972
+ * download-queue and shared-files requests, so this state must be fed by all of
973
+ * them (see UpdateState). Call only after sendRequest: connecting may have started
974
+ * a new session, which invalidates the previous state.
975
+ */
976
+ private getFileState;
960
977
  /**
961
978
  * Get updates for files, clients, servers, and friends.
962
979
  *
@@ -1005,7 +1022,12 @@ declare class AmuleClient {
1005
1022
  */
1006
1023
  downloadEd2kLink(link: string): Promise<void>;
1007
1024
  /**
1008
- * Get download queue
1025
+ * Get download queue (EC_OP_GET_DLOAD_QUEUE, full detail).
1026
+ *
1027
+ * The response rebases the per-connection diff state the daemon shares with
1028
+ * getUpdate()/getDownloadQueueWithSources(), so it is fed into the same
1029
+ * client-side state: both methods stay consistent however they are interleaved,
1030
+ * and chunkInfo/sourceNames are correct across repeated calls.
1009
1031
  */
1010
1032
  getDownloadQueue(): Promise<AmuleTransferringFile[]>;
1011
1033
  /**