@foss.global/codefeed 4.0.0 → 4.2.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/changelog.md +32 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.giteasourceinventory.d.ts +30 -6
- package/dist_ts/classes.giteasourceinventory.js +179 -23
- package/dist_ts/classes.giteasourcetransfer.d.ts +55 -3
- package/dist_ts/classes.giteasourcetransfer.js +155 -4
- package/dist_ts/classes.lfsbatchclient.js +17 -118
- package/dist_ts/forgeinventory.transfer.d.ts +75 -0
- package/dist_ts/forgeinventory.transfer.js +174 -0
- package/package.json +3 -3
- package/readme.md +16 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.giteasourceinventory.ts +188 -23
- package/ts/classes.giteasourcetransfer.ts +182 -4
- package/ts/classes.lfsbatchclient.ts +15 -98
- package/ts/forgeinventory.transfer.ts +203 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import * as transport from './forgeinventory.transport.js';
|
|
3
3
|
import * as validation from './forgeinventory.validation.js';
|
|
4
|
+
import { createTransferBounds, createTransferStream, discardBody, heldBackChunks } from './forgeinventory.transfer.js';
|
|
4
5
|
import { ForgeRequestClient } from './forgeinventory.client.js';
|
|
5
6
|
import type { IForgeRequestMetrics } from './forgeinventory.client.js';
|
|
6
7
|
|
|
@@ -82,8 +83,6 @@ type TBatchEntry = ILfsObjectId & { actions: Record<string, ILfsAction>; errorCo
|
|
|
82
83
|
const lfsMediaType = 'application/vnd.git-lfs+json';
|
|
83
84
|
/** Objects per batch request; the Git LFS reference server and Gitea accept 100. */
|
|
84
85
|
const maxBatchSize = 100;
|
|
85
|
-
/** Response bytes read from a PUT or verify answer before the rest is discarded. */
|
|
86
|
-
const maxDiscardedResponseBytes = 64 * 1024;
|
|
87
86
|
const maxActionHeaders = 32;
|
|
88
87
|
const maxHeaderValueLength = 8 * 1024;
|
|
89
88
|
const maxHrefLength = 8 * 1024;
|
|
@@ -179,44 +178,14 @@ const actionExpiry = (actionArg: Record<string, unknown>, receivedAtArg: number)
|
|
|
179
178
|
* wrong; otherwise it throws `lfs-size-overrun`, `lfs-short-body` or
|
|
180
179
|
* `lfs-digest-mismatch`.
|
|
181
180
|
*/
|
|
182
|
-
|
|
181
|
+
const verifiedChunks = (sourceArg: AsyncIterable<Uint8Array>, oidArg: string, sizeArg: number): AsyncGenerator<Uint8Array, void, undefined> => {
|
|
183
182
|
const hash = plugins.crypto.createHash('sha256');
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (bytes > sizeArg) throw new validation.ForgeInventoryError('lfs-size-overrun');
|
|
191
|
-
hash.update(chunk);
|
|
192
|
-
if (held) yield held;
|
|
193
|
-
held = chunk;
|
|
194
|
-
}
|
|
195
|
-
if (bytes !== sizeArg) throw new validation.ForgeInventoryError('lfs-short-body');
|
|
196
|
-
if (hash.digest('hex') !== oidArg) throw new validation.ForgeInventoryError('lfs-digest-mismatch');
|
|
197
|
-
if (held) yield held;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Reads and drops at most `maxDiscardedResponseBytes` of a response body that
|
|
202
|
-
* carries nothing the client uses, so its connection can be reused; a longer
|
|
203
|
-
* or failing body is discarded with its connection.
|
|
204
|
-
*/
|
|
205
|
-
const discardBody = async (bodyArg: AsyncIterable<Uint8Array> | null): Promise<void> => {
|
|
206
|
-
if (!bodyArg) return;
|
|
207
|
-
const chunks = bodyArg[Symbol.asyncIterator]();
|
|
208
|
-
let bytes = 0;
|
|
209
|
-
try {
|
|
210
|
-
while (bytes <= maxDiscardedResponseBytes) {
|
|
211
|
-
const result = await chunks.next();
|
|
212
|
-
if (result.done) return;
|
|
213
|
-
bytes += result.value.byteLength;
|
|
214
|
-
}
|
|
215
|
-
} catch {
|
|
216
|
-
// The status already decided the outcome.
|
|
217
|
-
} finally {
|
|
218
|
-
try { await chunks.return?.(); } catch { /* The connection is discarded. */ }
|
|
219
|
-
}
|
|
183
|
+
return heldBackChunks(sourceArg, sizeArg, { invalidChunk: 'invalid-lfs-chunk', overrun: 'lfs-size-overrun', short: 'lfs-short-body' }, {
|
|
184
|
+
inspect: (chunkArg) => hash.update(chunkArg),
|
|
185
|
+
complete: () => {
|
|
186
|
+
if (hash.digest('hex') !== oidArg) throw new validation.ForgeInventoryError('lfs-digest-mismatch');
|
|
187
|
+
},
|
|
188
|
+
});
|
|
220
189
|
};
|
|
221
190
|
|
|
222
191
|
/**
|
|
@@ -390,10 +359,8 @@ export class LfsBatchClient {
|
|
|
390
359
|
transfer.end();
|
|
391
360
|
}
|
|
392
361
|
};
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
stream.once('close', () => transfer.end());
|
|
396
|
-
return { oid, size, status: 'available', stream };
|
|
362
|
+
// Destroying the stream ends the download and closes its connection at once, also while a read is pending.
|
|
363
|
+
return { oid, size, status: 'available', stream: createTransferStream(transfer, chunks()) };
|
|
397
364
|
} catch (error) {
|
|
398
365
|
transfer.end();
|
|
399
366
|
throw transfer.failure(error, 'lfs-download-failed');
|
|
@@ -553,62 +520,12 @@ export class LfsBatchClient {
|
|
|
553
520
|
|
|
554
521
|
/**
|
|
555
522
|
* The bounds of one transfer: the caller's signal, a wall-clock limit and an
|
|
556
|
-
* idle limit
|
|
557
|
-
*
|
|
523
|
+
* idle limit of `requestTimeoutMs`; a passed limit fails with
|
|
524
|
+
* `lfs-transfer-timed-out`.
|
|
558
525
|
*/
|
|
559
526
|
#transfer(signalArg: AbortSignal | undefined, limitMsArg = this.#limits.transferTimeoutMs) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
reason ??= reasonArg;
|
|
564
|
-
controller.abort();
|
|
565
|
-
};
|
|
566
|
-
const abort = () => stop('request-aborted');
|
|
567
|
-
if (signalArg?.aborted) abort();
|
|
568
|
-
signalArg?.addEventListener('abort', abort, { once: true });
|
|
569
|
-
const deadline = setTimeout(() => stop('lfs-transfer-timed-out'), limitMsArg);
|
|
570
|
-
let idle: ReturnType<typeof setTimeout> | undefined;
|
|
571
|
-
const rearm = () => {
|
|
572
|
-
clearTimeout(idle);
|
|
573
|
-
if (!controller.signal.aborted) idle = setTimeout(() => stop('lfs-transfer-timed-out'), this.#limits.requestTimeoutMs);
|
|
574
|
-
};
|
|
575
|
-
rearm();
|
|
576
|
-
const end = () => {
|
|
577
|
-
clearTimeout(deadline);
|
|
578
|
-
clearTimeout(idle);
|
|
579
|
-
signalArg?.removeEventListener('abort', abort);
|
|
580
|
-
stop('transfer-ended');
|
|
581
|
-
};
|
|
582
|
-
const failure = (errorArg: unknown, fallbackArg: string): validation.ForgeInventoryError => {
|
|
583
|
-
if (reason !== null && reason !== 'transfer-ended') return new validation.ForgeInventoryError(reason);
|
|
584
|
-
return errorArg instanceof validation.ForgeInventoryError ? errorArg : new validation.ForgeInventoryError(fallbackArg);
|
|
585
|
-
};
|
|
586
|
-
return {
|
|
587
|
-
signal: controller.signal,
|
|
588
|
-
end,
|
|
589
|
-
failure,
|
|
590
|
-
/** Runs one step of the transfer; it rejects as soon as the transfer ends, even if the step does not. */
|
|
591
|
-
run: async <T>(stepArg: () => Promise<T>): Promise<T> => {
|
|
592
|
-
if (controller.signal.aborted) throw failure(undefined, 'request-aborted');
|
|
593
|
-
let onAbort!: () => void;
|
|
594
|
-
const aborted = new Promise<never>((_resolve, reject) => {
|
|
595
|
-
onAbort = () => reject(failure(undefined, 'request-aborted'));
|
|
596
|
-
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
597
|
-
});
|
|
598
|
-
try {
|
|
599
|
-
return await Promise.race([stepArg(), aborted]);
|
|
600
|
-
} finally {
|
|
601
|
-
controller.signal.removeEventListener('abort', onAbort);
|
|
602
|
-
}
|
|
603
|
-
},
|
|
604
|
-
/** Passes chunks on and re-arms the idle limit on each one. */
|
|
605
|
-
watch: async function* (sourceArg: AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array, void, undefined> {
|
|
606
|
-
for await (const chunk of sourceArg) {
|
|
607
|
-
if (controller.signal.aborted) throw failure(undefined, 'lfs-transfer-failed');
|
|
608
|
-
rearm();
|
|
609
|
-
yield chunk;
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
};
|
|
527
|
+
return createTransferBounds(signalArg, {
|
|
528
|
+
limitMs: limitMsArg, idleMs: this.#limits.requestTimeoutMs, timedOutCode: 'lfs-transfer-timed-out', failedCode: 'lfs-transfer-failed',
|
|
529
|
+
});
|
|
613
530
|
}
|
|
614
531
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import * as validation from './forgeinventory.validation.js';
|
|
3
|
+
|
|
4
|
+
/** Response bytes read from an unused answer before the rest is discarded. */
|
|
5
|
+
const maxDiscardedResponseBytes = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reads and drops at most 64 KiB of a response body that carries nothing the
|
|
9
|
+
* caller uses, so its connection can be reused; a longer or failing body is
|
|
10
|
+
* discarded with its connection.
|
|
11
|
+
*/
|
|
12
|
+
export const discardBody = async (bodyArg: AsyncIterable<Uint8Array> | null): Promise<void> => {
|
|
13
|
+
if (!bodyArg) return;
|
|
14
|
+
const chunks = bodyArg[Symbol.asyncIterator]();
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
try {
|
|
17
|
+
while (bytes <= maxDiscardedResponseBytes) {
|
|
18
|
+
const result = await chunks.next();
|
|
19
|
+
if (result.done) return;
|
|
20
|
+
bytes += result.value.byteLength;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// The status already decided the outcome.
|
|
24
|
+
} finally {
|
|
25
|
+
try { await chunks.return?.(); } catch { /* The connection is discarded. */ }
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface ITransferBoundsOptions {
|
|
30
|
+
/** Wall-clock limit of the whole transfer. */
|
|
31
|
+
limitMs: number;
|
|
32
|
+
/** Longest gap between two chunks of a watched source. */
|
|
33
|
+
idleMs: number;
|
|
34
|
+
/** The code a passed wall-clock or idle limit fails with. */
|
|
35
|
+
timedOutCode: string;
|
|
36
|
+
/** The code a watched source that goes on after the transfer ended fails with. */
|
|
37
|
+
failedCode: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ITransferBounds {
|
|
41
|
+
/** Aborts once the caller aborts, a limit passes or the transfer ends. */
|
|
42
|
+
signal: AbortSignal;
|
|
43
|
+
/** Ends the transfer: clears its timers and aborts `signal`. Idempotent. */
|
|
44
|
+
end: () => void;
|
|
45
|
+
/**
|
|
46
|
+
* The error the transfer fails with: the reason that ended it (`request-aborted`
|
|
47
|
+
* or the timeout code), else `errorArg` when it is a `ForgeInventoryError`,
|
|
48
|
+
* else `fallbackArg`.
|
|
49
|
+
*/
|
|
50
|
+
failure: (errorArg: unknown, fallbackArg: string) => validation.ForgeInventoryError;
|
|
51
|
+
/** Runs one step of the transfer; it rejects as soon as the transfer ends, even if the step does not. */
|
|
52
|
+
run: <T>(stepArg: () => Promise<T>) => Promise<T>;
|
|
53
|
+
/** Re-arms the idle limit, before a step that waits on the network without a watched source. */
|
|
54
|
+
touch: () => void;
|
|
55
|
+
/** Passes chunks on and re-arms the idle limit on each one. */
|
|
56
|
+
watch: (sourceArg: AsyncIterable<Uint8Array>) => AsyncGenerator<Uint8Array, void, undefined>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The bounds of one streamed transfer: the caller's signal, a wall-clock limit
|
|
61
|
+
* and an idle limit that `watch` re-arms on every chunk. Whichever ends it
|
|
62
|
+
* first decides the failure code. Every transfer must be ended with `end`.
|
|
63
|
+
*/
|
|
64
|
+
export const createTransferBounds = (signalArg: AbortSignal | undefined, optionsArg: ITransferBoundsOptions): ITransferBounds => {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
let reason: string | null = null;
|
|
67
|
+
const stop = (reasonArg: string) => {
|
|
68
|
+
reason ??= reasonArg;
|
|
69
|
+
controller.abort();
|
|
70
|
+
};
|
|
71
|
+
const abort = () => stop('request-aborted');
|
|
72
|
+
if (signalArg?.aborted) abort();
|
|
73
|
+
signalArg?.addEventListener('abort', abort, { once: true });
|
|
74
|
+
const deadline = setTimeout(() => stop(optionsArg.timedOutCode), optionsArg.limitMs);
|
|
75
|
+
let idle: ReturnType<typeof setTimeout> | undefined;
|
|
76
|
+
const rearm = () => {
|
|
77
|
+
clearTimeout(idle);
|
|
78
|
+
if (!controller.signal.aborted) idle = setTimeout(() => stop(optionsArg.timedOutCode), optionsArg.idleMs);
|
|
79
|
+
};
|
|
80
|
+
rearm();
|
|
81
|
+
const end = () => {
|
|
82
|
+
clearTimeout(deadline);
|
|
83
|
+
clearTimeout(idle);
|
|
84
|
+
signalArg?.removeEventListener('abort', abort);
|
|
85
|
+
stop('transfer-ended');
|
|
86
|
+
};
|
|
87
|
+
const failure = (errorArg: unknown, fallbackArg: string): validation.ForgeInventoryError => {
|
|
88
|
+
if (reason !== null && reason !== 'transfer-ended') return new validation.ForgeInventoryError(reason);
|
|
89
|
+
return errorArg instanceof validation.ForgeInventoryError ? errorArg : new validation.ForgeInventoryError(fallbackArg);
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
end,
|
|
94
|
+
failure,
|
|
95
|
+
touch: rearm,
|
|
96
|
+
run: async <T>(stepArg: () => Promise<T>): Promise<T> => {
|
|
97
|
+
if (controller.signal.aborted) throw failure(undefined, 'request-aborted');
|
|
98
|
+
let onAbort!: () => void;
|
|
99
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
100
|
+
onAbort = () => reject(failure(undefined, 'request-aborted'));
|
|
101
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
return await Promise.race([stepArg(), aborted]);
|
|
105
|
+
} finally {
|
|
106
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
watch: async function* (sourceArg: AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array, void, undefined> {
|
|
110
|
+
for await (const chunk of sourceArg) {
|
|
111
|
+
if (controller.signal.aborted) throw failure(undefined, optionsArg.failedCode);
|
|
112
|
+
rearm();
|
|
113
|
+
yield chunk;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export interface IHeldBackCodes {
|
|
120
|
+
/** A chunk that is not a `Uint8Array`. */
|
|
121
|
+
invalidChunk: string;
|
|
122
|
+
/** More bytes than the declared size. */
|
|
123
|
+
overrun: string;
|
|
124
|
+
/** Fewer bytes than the declared size. */
|
|
125
|
+
short: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface IHeldBackChecks {
|
|
129
|
+
/** Sees every non-empty chunk in order, before it is passed on. */
|
|
130
|
+
inspect?: (chunkArg: Uint8Array) => void;
|
|
131
|
+
/** Runs once exactly `size` bytes have arrived, before the last chunk is passed on; a rejection keeps it back. */
|
|
132
|
+
complete?: () => void | Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Passes the bytes of `sourceArg` on while it counts them. The last chunk is
|
|
137
|
+
* held back until the source has ended with exactly `sizeArg` bytes and
|
|
138
|
+
* `checksArg.complete` has passed, so a consumer never receives a complete
|
|
139
|
+
* body that is wrong; otherwise it throws `codesArg.overrun` as soon as the
|
|
140
|
+
* size is exceeded, `codesArg.short` when the source ends early, or what
|
|
141
|
+
* `complete` threw.
|
|
142
|
+
*/
|
|
143
|
+
export async function* heldBackChunks(
|
|
144
|
+
sourceArg: AsyncIterable<Uint8Array>, sizeArg: number, codesArg: IHeldBackCodes, checksArg: IHeldBackChecks = {},
|
|
145
|
+
): AsyncGenerator<Uint8Array, void, undefined> {
|
|
146
|
+
let bytes = 0;
|
|
147
|
+
let held: Uint8Array | null = null;
|
|
148
|
+
for await (const chunk of sourceArg) {
|
|
149
|
+
if (!(chunk instanceof Uint8Array)) throw new validation.ForgeInventoryError(codesArg.invalidChunk);
|
|
150
|
+
if (chunk.byteLength === 0) continue;
|
|
151
|
+
bytes += chunk.byteLength;
|
|
152
|
+
if (bytes > sizeArg) throw new validation.ForgeInventoryError(codesArg.overrun);
|
|
153
|
+
checksArg.inspect?.(chunk);
|
|
154
|
+
if (held) yield held;
|
|
155
|
+
held = chunk;
|
|
156
|
+
}
|
|
157
|
+
if (bytes !== sizeArg) throw new validation.ForgeInventoryError(codesArg.short);
|
|
158
|
+
await checksArg.complete?.();
|
|
159
|
+
if (held) yield held;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The readable stream of one transfer's bytes. It pulls `chunksArg` with
|
|
164
|
+
* backpressure and ends `boundsArg` and calls `onEndArg` as soon as it is
|
|
165
|
+
* destroyed, also while a pull is pending: ending the bounds aborts the
|
|
166
|
+
* request, which closes its connection, and the pending pull's result is
|
|
167
|
+
* dropped. (`Readable.from` waits for a pending pull before it finishes a
|
|
168
|
+
* destroy, so a stalled body would keep its connection until the idle limit.)
|
|
169
|
+
* The stream errors with what `chunksArg` throws.
|
|
170
|
+
*/
|
|
171
|
+
export const createTransferStream = (
|
|
172
|
+
boundsArg: ITransferBounds, chunksArg: AsyncGenerator<Uint8Array, void, undefined>, onEndArg?: () => void,
|
|
173
|
+
): InstanceType<typeof plugins.stream.Readable> => {
|
|
174
|
+
let pulling = false;
|
|
175
|
+
const stream: InstanceType<typeof plugins.stream.Readable> = new plugins.stream.Readable({
|
|
176
|
+
read() {
|
|
177
|
+
if (pulling) return;
|
|
178
|
+
pulling = true;
|
|
179
|
+
void (async () => {
|
|
180
|
+
try {
|
|
181
|
+
while (!stream.destroyed) {
|
|
182
|
+
const result = await chunksArg.next();
|
|
183
|
+
if (stream.destroyed) return;
|
|
184
|
+
if (result.done) return void stream.push(null);
|
|
185
|
+
if (!stream.push(result.value)) return;
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
stream.destroy(error as Error);
|
|
189
|
+
} finally {
|
|
190
|
+
pulling = false;
|
|
191
|
+
}
|
|
192
|
+
})();
|
|
193
|
+
},
|
|
194
|
+
destroy(errorArg, callbackArg) {
|
|
195
|
+
boundsArg.end();
|
|
196
|
+
onEndArg?.();
|
|
197
|
+
// Runs the generator's cleanup once a pending pull has settled; nothing waits for it.
|
|
198
|
+
chunksArg.return(undefined).catch(() => undefined);
|
|
199
|
+
callbackArg(errorArg);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return stream;
|
|
203
|
+
};
|