utilium 3.6.2 → 3.7.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/dist/array.d.ts +1 -0
- package/dist/array.js +8 -1
- package/dist/cache.d.ts +29 -0
- package/dist/cache.js +110 -64
- package/package.json +1 -1
package/dist/array.d.ts
CHANGED
package/dist/array.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export function chunkArray(array, chunkSize) {
|
|
2
|
+
if (!Number.isSafeInteger(chunkSize) || chunkSize <= 0)
|
|
3
|
+
throw new Error('chunkArray: chunk size must be a positive integer');
|
|
4
|
+
const chunks = [];
|
|
5
|
+
for (let i = 0; i < array.length; i += chunkSize)
|
|
6
|
+
chunks.push(array.slice(i, i + chunkSize));
|
|
7
|
+
return chunks;
|
|
8
|
+
}
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ranged cache
|
|
3
|
+
* Copyright (c) 2025 James Prevett
|
|
4
|
+
*/
|
|
1
5
|
export interface Options {
|
|
2
6
|
/**
|
|
3
7
|
* If true, use multiple buffers to cache a file.
|
|
@@ -17,11 +21,20 @@ export interface Options {
|
|
|
17
21
|
* @default false
|
|
18
22
|
*/
|
|
19
23
|
cacheOnly?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Multiplier applied to a region's capacity when it has to grow.
|
|
26
|
+
* Values outside `[1, 100]` fall back to the default.
|
|
27
|
+
* Higher trades memory for fewer reallocations, while `1` allocates exactly what is needed and makes repeated appends quadratic.
|
|
28
|
+
* @default 2
|
|
29
|
+
*/
|
|
30
|
+
growFactor?: number;
|
|
20
31
|
}
|
|
21
32
|
export type Range = {
|
|
22
33
|
start: number;
|
|
23
34
|
end: number;
|
|
24
35
|
};
|
|
36
|
+
/** Insert `[start, end)` into a sorted, non-overlapping list of ranges, merging it with any ranges it touches. */
|
|
37
|
+
export declare function addRange(ranges: Range[], start: number, end: number): void;
|
|
25
38
|
export interface Region {
|
|
26
39
|
/** The region's offset from the start of the resource */
|
|
27
40
|
offset: number;
|
|
@@ -47,8 +60,20 @@ export declare class Resource<ID> {
|
|
|
47
60
|
constructor(
|
|
48
61
|
/** The resource ID */
|
|
49
62
|
id: ID, _size: number, options: Options, resources?: Map<ID, Resource<ID> | undefined>);
|
|
63
|
+
/**
|
|
64
|
+
* Resize `region.data` to `length` bytes.
|
|
65
|
+
* Capacity can be over-allocated geometrically so appending to a region repeatedly stays amortized O(1).
|
|
66
|
+
*/
|
|
67
|
+
protected resizeRegion(region: Region, length: number): void;
|
|
68
|
+
/** Merge the region after `index` into the one at `index`, if the gap between them is small enough */
|
|
69
|
+
protected mergeAt(index: number): boolean;
|
|
50
70
|
/** Combines adjacent regions and combines adjacent ranges within a region */
|
|
51
71
|
collect(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Combine the region at `index` with the neighbors it now reaches.
|
|
74
|
+
* Only that region can have changed, so this does not re-walk the whole list the way `collect` does.
|
|
75
|
+
*/
|
|
76
|
+
protected collectAt(index: number): void;
|
|
52
77
|
/** Takes an initial range and finds the sub-ranges that are not in the cache */
|
|
53
78
|
missing(start: number, end: number): Range[];
|
|
54
79
|
/**
|
|
@@ -56,8 +81,12 @@ export declare class Resource<ID> {
|
|
|
56
81
|
* This is conceptually the inverse of `missing`.
|
|
57
82
|
*/
|
|
58
83
|
cached(start: number, end: number): Range[];
|
|
84
|
+
/** Index of the first region positioned after `offset`. Regions are kept sorted, so this is a binary search. */
|
|
85
|
+
protected indexAfter(offset: number): number;
|
|
59
86
|
/** Get the region who's ranges include an offset */
|
|
60
87
|
regionAt(offset: number): Region | undefined;
|
|
88
|
+
/** The regions overlapping `[start, end)`, in order. Seeks to the first one rather than scanning from the start. */
|
|
89
|
+
regionsIn(start: number, end: number): Generator<Region>;
|
|
61
90
|
/** Add new data to the cache at given specified offset */
|
|
62
91
|
add(data: Uint8Array, offset: number): this;
|
|
63
92
|
}
|
package/dist/cache.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/** Insert `[start, end)` into a sorted, non-overlapping list of ranges, merging it with any ranges it touches. */
|
|
2
|
+
export function addRange(ranges, start, end) {
|
|
3
|
+
let i = ranges.length;
|
|
4
|
+
while (i > 0 && ranges[i - 1].start > start)
|
|
5
|
+
i--;
|
|
6
|
+
const prev = ranges[i - 1];
|
|
7
|
+
if (prev && prev.end >= start) {
|
|
8
|
+
if (prev.end >= end)
|
|
9
|
+
return;
|
|
10
|
+
prev.end = end;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
ranges.splice(i, 0, { start, end });
|
|
14
|
+
i++;
|
|
15
|
+
}
|
|
16
|
+
const merged = ranges[i - 1];
|
|
17
|
+
let j = i;
|
|
18
|
+
while (j < ranges.length && ranges[j].start <= merged.end) {
|
|
19
|
+
merged.end = Math.max(merged.end, ranges[j].end);
|
|
20
|
+
j++;
|
|
21
|
+
}
|
|
22
|
+
if (j > i)
|
|
23
|
+
ranges.splice(i, j - i);
|
|
24
|
+
}
|
|
7
25
|
/**
|
|
8
26
|
* The cache for a specific resource
|
|
9
27
|
* @internal
|
|
@@ -51,50 +69,71 @@ export class Resource {
|
|
|
51
69
|
this._size = _size;
|
|
52
70
|
this.options = options;
|
|
53
71
|
options.sparse ??= true;
|
|
72
|
+
if (!options.growFactor
|
|
73
|
+
|| !Number.isFinite(options.growFactor)
|
|
74
|
+
|| options.growFactor < 1
|
|
75
|
+
|| options.growFactor > 100)
|
|
76
|
+
options.growFactor = 2;
|
|
54
77
|
if (!options.sparse)
|
|
55
78
|
this.regions.push({ offset: 0, data: new Uint8Array(_size), ranges: [] });
|
|
56
79
|
resources?.set(id, this);
|
|
57
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Resize `region.data` to `length` bytes.
|
|
83
|
+
* Capacity can be over-allocated geometrically so appending to a region repeatedly stays amortized O(1).
|
|
84
|
+
*/
|
|
85
|
+
resizeRegion(region, length) {
|
|
86
|
+
const { buffer, byteOffset } = region.data;
|
|
87
|
+
if (buffer.byteLength - byteOffset >= length) {
|
|
88
|
+
region.data = new Uint8Array(buffer, byteOffset, length);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const grown = new Uint8Array(Math.max(length, (buffer.byteLength - byteOffset) * this.options.growFactor));
|
|
92
|
+
grown.set(region.data);
|
|
93
|
+
region.data = grown.subarray(0, length);
|
|
94
|
+
}
|
|
95
|
+
/** Merge the region after `index` into the one at `index`, if the gap between them is small enough */
|
|
96
|
+
mergeAt(index) {
|
|
97
|
+
const current = this.regions[index];
|
|
98
|
+
const next = this.regions[index + 1];
|
|
99
|
+
if (!current || !next)
|
|
100
|
+
return false;
|
|
101
|
+
const { regionGapThreshold = 0xfff } = this.options;
|
|
102
|
+
if (next.offset - (current.offset + current.data.byteLength) > regionGapThreshold)
|
|
103
|
+
return false;
|
|
104
|
+
for (const range of next.ranges)
|
|
105
|
+
addRange(current.ranges, range.start, range.end);
|
|
106
|
+
const length = next.offset + next.data.byteLength - current.offset;
|
|
107
|
+
if (length > current.data.byteLength)
|
|
108
|
+
this.resizeRegion(current, length);
|
|
109
|
+
current.data.set(next.data, next.offset - current.offset);
|
|
110
|
+
this.regions.splice(index + 1, 1);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
58
113
|
/** Combines adjacent regions and combines adjacent ranges within a region */
|
|
59
114
|
collect() {
|
|
60
115
|
if (!this.options.sparse)
|
|
61
116
|
return;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const current = this.regions[i];
|
|
65
|
-
const next = this.regions[i + 1];
|
|
66
|
-
if (next.offset - (current.offset + current.data.byteLength) > regionGapThreshold) {
|
|
117
|
+
for (let i = 0; i < this.regions.length - 1;)
|
|
118
|
+
if (!this.mergeAt(i))
|
|
67
119
|
i++;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
return acc;
|
|
82
|
-
}, []);
|
|
83
|
-
// Extend buffer to include the new region. `current.data` starts at
|
|
84
|
-
// `current.offset`, so its length is measured from there (the `.set()`
|
|
85
|
-
// destination is already relative to `current.offset`).
|
|
86
|
-
current.data = extendBuffer(current.data, next.offset + next.data.byteLength - current.offset);
|
|
87
|
-
current.data.set(next.data, next.offset - current.offset);
|
|
88
|
-
// Remove the next region after merging
|
|
89
|
-
this.regions.splice(i + 1, 1);
|
|
90
|
-
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Combine the region at `index` with the neighbors it now reaches.
|
|
123
|
+
* Only that region can have changed, so this does not re-walk the whole list the way `collect` does.
|
|
124
|
+
*/
|
|
125
|
+
collectAt(index) {
|
|
126
|
+
if (!this.options.sparse)
|
|
127
|
+
return;
|
|
128
|
+
if (index > 0 && this.mergeAt(index - 1))
|
|
129
|
+
index--;
|
|
130
|
+
while (this.mergeAt(index))
|
|
131
|
+
;
|
|
91
132
|
}
|
|
92
133
|
/** Takes an initial range and finds the sub-ranges that are not in the cache */
|
|
93
134
|
missing(start, end) {
|
|
94
135
|
const missingRanges = [];
|
|
95
|
-
for (const region of this.
|
|
96
|
-
if (region.offset >= end)
|
|
97
|
-
break;
|
|
136
|
+
for (const region of this.regionsIn(start, end)) {
|
|
98
137
|
for (const range of region.ranges) {
|
|
99
138
|
if (range.end <= start)
|
|
100
139
|
continue;
|
|
@@ -123,9 +162,7 @@ export class Resource {
|
|
|
123
162
|
*/
|
|
124
163
|
cached(start, end) {
|
|
125
164
|
const cachedRanges = [];
|
|
126
|
-
for (const region of this.
|
|
127
|
-
if (region.offset >= end)
|
|
128
|
-
break;
|
|
165
|
+
for (const region of this.regionsIn(start, end)) {
|
|
129
166
|
for (const range of region.ranges) {
|
|
130
167
|
if (range.end <= start)
|
|
131
168
|
continue;
|
|
@@ -150,43 +187,52 @@ export class Resource {
|
|
|
150
187
|
}
|
|
151
188
|
return merged;
|
|
152
189
|
}
|
|
190
|
+
/** Index of the first region positioned after `offset`. Regions are kept sorted, so this is a binary search. */
|
|
191
|
+
indexAfter(offset) {
|
|
192
|
+
let low = 0, high = this.regions.length;
|
|
193
|
+
while (low < high) {
|
|
194
|
+
const mid = (low + high) >> 1;
|
|
195
|
+
if (this.regions[mid].offset > offset)
|
|
196
|
+
high = mid;
|
|
197
|
+
else
|
|
198
|
+
low = mid + 1;
|
|
199
|
+
}
|
|
200
|
+
return low;
|
|
201
|
+
}
|
|
153
202
|
/** Get the region who's ranges include an offset */
|
|
154
203
|
regionAt(offset) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
204
|
+
const region = this.regions[this.indexAfter(offset) - 1];
|
|
205
|
+
if (region && offset < region.offset + region.data.byteLength)
|
|
206
|
+
return region;
|
|
207
|
+
}
|
|
208
|
+
/** The regions overlapping `[start, end)`, in order. Seeks to the first one rather than scanning from the start. */
|
|
209
|
+
*regionsIn(start, end) {
|
|
210
|
+
for (let i = Math.max(0, this.indexAfter(start) - 1); i < this.regions.length; i++) {
|
|
211
|
+
const region = this.regions[i];
|
|
212
|
+
if (region.offset >= end)
|
|
213
|
+
return;
|
|
214
|
+
if (region.offset + region.data.byteLength > start)
|
|
215
|
+
yield region;
|
|
163
216
|
}
|
|
164
217
|
}
|
|
165
218
|
/** Add new data to the cache at given specified offset */
|
|
166
219
|
add(data, offset) {
|
|
167
220
|
const end = offset + data.byteLength;
|
|
168
|
-
const
|
|
169
|
-
|
|
221
|
+
const index = this.indexAfter(offset) - 1;
|
|
222
|
+
const region = this.regions[index];
|
|
223
|
+
if (region && offset < region.offset + region.data.byteLength) {
|
|
170
224
|
// `region.data` holds bytes starting at `region.offset`, so positions
|
|
171
225
|
// within it are relative to `region.offset` (not absolute resource offsets).
|
|
172
|
-
|
|
226
|
+
if (end - region.offset > region.data.byteLength)
|
|
227
|
+
this.resizeRegion(region, end - region.offset);
|
|
173
228
|
region.data.set(data, offset - region.offset);
|
|
174
|
-
region.ranges
|
|
175
|
-
|
|
176
|
-
this.collect();
|
|
229
|
+
addRange(region.ranges, offset, end);
|
|
230
|
+
this.collectAt(index);
|
|
177
231
|
return this;
|
|
178
232
|
}
|
|
179
|
-
// Find the correct index to insert the new region
|
|
180
|
-
const newRegion = { data, offset: offset, ranges: [{ start: offset, end }] };
|
|
181
|
-
const insertIndex = this.regions.findIndex(region => region.offset > offset);
|
|
182
233
|
// Insert at the right index to keep regions sorted
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
this.regions.splice(insertIndex, 0, newRegion); // Insert before the first region with a greater offset
|
|
188
|
-
}
|
|
189
|
-
this.collect();
|
|
234
|
+
this.regions.splice(index + 1, 0, { data, offset, ranges: [{ start: offset, end }] });
|
|
235
|
+
this.collectAt(index + 1);
|
|
190
236
|
return this;
|
|
191
237
|
}
|
|
192
238
|
}
|