amule-ec-client 0.6.1 → 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 (45) hide show
  1. package/README.md +10 -2
  2. package/dist/{AddLinkRequest-D2IME4eb.js → AddLinkRequest-CGv1H3PC.js} +2 -3
  3. package/dist/{ClientQueueRequest-DFfCiNcz.js → ClientQueueRequest-mklt-KLi.js} +2 -3
  4. package/dist/{ClientQueueResponse-JTrBqrr1.js → ClientQueueResponse-RfXh2JGa.js} +8 -5
  5. package/dist/{CreateCategoryRequest-CRUMcPo5.js → CreateCategoryRequest-Cyp9G7jU.js} +2 -3
  6. package/dist/{DeleteCategoryRequest-C8v9oGZ-.js → DeleteCategoryRequest-DbLJ4mvi.js} +2 -3
  7. package/dist/{DownloadCommandRequest-B3KV_bvm.js → DownloadCommandRequest-DLU7RtYs.js} +2 -3
  8. package/dist/{DownloadFileRequest-BjrroO3h.js → DownloadFileRequest-Db7oijyu.js} +2 -3
  9. package/dist/{DownloadQueueRequest-Dil5hl3D.js → DownloadQueueRequest-DdHRjl4L.js} +2 -3
  10. package/dist/{GetPreferencesRequest-DsxlghqT.js → GetPreferencesRequest-BlWGYNCP.js} +2 -3
  11. package/dist/{PreferencesDetailsResponse-DapscVQi.js → PreferencesDetailsResponse-BX6qUoga.js} +1 -2
  12. package/dist/{PreferencesResponse-8n1STWUZ.js → PreferencesResponse-migFtIG7.js} +1 -2
  13. package/dist/{ReloadSharedFilesRequest-kt0Pxs8A.js → ReloadSharedFilesRequest-WD9iYLje.js} +2 -2
  14. package/dist/{Request-C0UUuQIX.js → Request-YQ0nIM6b.js} +3 -9
  15. package/dist/{SearchProgressResponse-C1CdzKnz.js → SearchProgressResponse-CkEgZGFE.js} +1 -2
  16. package/dist/{SearchRequest-DQnKfA5Z.js → SearchRequest-DaAKS4IO.js} +2 -3
  17. package/dist/{SearchResultsRequest-2ejFGQmQ.js → SearchResultsRequest-B53jwjRH.js} +2 -2
  18. package/dist/{SearchResultsResponse-DwmvbQop.js → SearchResultsResponse-C1l-bBja.js} +1 -2
  19. package/dist/{SearchStatusRequest-B_xr4-72.js → SearchStatusRequest-DuEa5ZYu.js} +2 -2
  20. package/dist/{SearchStopRequest-8hwXobty.js → SearchStopRequest-B5TZdMiB.js} +2 -2
  21. package/dist/{ServerConnectRequest-BhNpGiEJ.js → ServerConnectRequest-BeB8RSBN.js} +2 -3
  22. package/dist/{ServerDisconnectRequest-BjCitLtU.js → ServerDisconnectRequest-CSkUHETP.js} +2 -2
  23. package/dist/{ServerListRequest-BWXLGJib.js → ServerListRequest-B82686jD.js} +2 -2
  24. package/dist/{ServerListResponse-BUJ7sz5p.js → ServerListResponse-M3C2OcfZ.js} +5 -4
  25. package/dist/{ServerUpdateFromUrlRequest-K8C_d0UQ.js → ServerUpdateFromUrlRequest-BcvhRKgH.js} +2 -3
  26. package/dist/{SetFileCategoryRequest-VwroLBTV.js → SetFileCategoryRequest-C1jWsYO5.js} +2 -3
  27. package/dist/{SetPreferencesRequest-KZ23VaBY.js → SetPreferencesRequest-CNJVmeQl.js} +2 -3
  28. package/dist/{SharedFilePriorityRequest-7BriqAcr.js → SharedFilePriorityRequest-CBqkaXaK.js} +2 -3
  29. package/dist/{SharedFilesRequest-B72XyRcA.js → SharedFilesRequest-5mKKjzgt.js} +2 -3
  30. package/dist/SharedFilesResponse-CFl0NuO-.js +45 -0
  31. package/dist/{StatsRequest-CGGumOOx.js → StatsRequest-C9gBUvSq.js} +2 -3
  32. package/dist/{StatsResponse-DxQk686g.js → StatsResponse-KOk-xT6_.js} +25 -2
  33. package/dist/{Codes-CjiW7Pdl.js → Tag-D_Ybe5RQ.js} +280 -1
  34. package/dist/{UpdateCategoryRequest-CzKJa_rC.js → UpdateCategoryRequest-BOYfb6Vn.js} +2 -3
  35. package/dist/{UpdateRequest-g2ReOQ5m.js → UpdateRequest-csLQz7JO.js} +2 -3
  36. package/dist/UpdateState-LhdtiATk.js +443 -0
  37. package/dist/index.d.ts +381 -337
  38. package/dist/index.js +116 -55
  39. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  40. package/dist/{utils-DBnUQnZ-.js → utils-CNoUroOI.js} +11 -1
  41. package/package.json +1 -1
  42. package/dist/DownloadQueueResponse-DUam_zNf.js +0 -259
  43. package/dist/SharedFilesResponse-CkqvYd3N.js +0 -39
  44. package/dist/Tag-CZF6BRMY.js +0 -260
  45. package/dist/UpdateResponse-CGwVjn0Y.js +0 -45
@@ -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 };