tiny-essentials 1.29.1 → 1.29.2
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/1/29/2.md +82 -0
- package/dist/v1/TinyBasicsEs.min.js +1 -1
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyFetchLoader.min.js +1 -0
- package/dist/v1/TinyImageLoader.min.js +1 -0
- package/dist/v1/TinyMediaCache.min.js +1 -0
- package/dist/v1/TinyMediaLoader.min.js +1 -0
- package/dist/v1/basics/index.cjs +5 -0
- package/dist/v1/basics/index.d.mts +2 -1
- package/dist/v1/basics/index.d.mts.map +1 -1
- package/dist/v1/basics/index.mjs +2 -1
- package/dist/v1/basics/media/youtube.cjs +275 -0
- package/dist/v1/basics/media/youtube.d.mts +107 -0
- package/dist/v1/basics/media/youtube.d.mts.map +1 -0
- package/dist/v1/basics/media/youtube.mjs +214 -0
- package/dist/v1/index.cjs +13 -0
- package/dist/v1/index.d.mts +6 -1
- package/dist/v1/index.d.mts.map +1 -1
- package/dist/v1/index.mjs +7 -2
- package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.cjs +0 -1
- package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.d.mts +0 -1
- package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.d.mts.map +1 -1
- package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.mjs +0 -1
- package/dist/v1/libs/fs/plugins/OPFS/index.cjs +0 -8
- package/dist/v1/libs/fs/plugins/OPFS/index.d.mts +0 -8
- package/dist/v1/libs/fs/plugins/OPFS/index.d.mts.map +1 -1
- package/dist/v1/libs/fs/plugins/OPFS/index.mjs +0 -8
- package/dist/v1/libs/html/TinyHtml.cjs +1 -1
- package/dist/v1/libs/html/TinyHtml.d.mts +2 -2
- package/dist/v1/libs/html/TinyHtml.d.mts.map +1 -1
- package/dist/v1/libs/html/TinyHtml.mjs +1 -1
- package/dist/v1/libs/html/loader/TinyMediaLoader.cjs +1999 -0
- package/dist/v1/libs/html/loader/TinyMediaLoader.d.mts +999 -0
- package/dist/v1/libs/html/loader/TinyMediaLoader.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/TinyMediaLoader.mjs +1852 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.cjs +77 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.d.mts +103 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.mjs +67 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.cjs +630 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.d.mts +138 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.mjs +576 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.cjs +70 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.d.mts +100 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.mjs +60 -0
- package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.cjs +946 -0
- package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.d.mts +67 -0
- package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.mjs +896 -0
- package/dist/v1/libs/html/loader/formats/TinyFetchLoader.cjs +225 -0
- package/dist/v1/libs/html/loader/formats/TinyFetchLoader.d.mts +149 -0
- package/dist/v1/libs/html/loader/formats/TinyFetchLoader.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/formats/TinyFetchLoader.mjs +197 -0
- package/dist/v1/libs/html/loader/formats/TinyImageLoader.cjs +1454 -0
- package/dist/v1/libs/html/loader/formats/TinyImageLoader.d.mts +200 -0
- package/dist/v1/libs/html/loader/formats/TinyImageLoader.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/formats/TinyImageLoader.mjs +1317 -0
- package/dist/v1/libs/html/loader/probe/TinyImageProbe.cjs +668 -0
- package/dist/v1/libs/html/loader/probe/TinyImageProbe.d.mts +83 -0
- package/dist/v1/libs/html/loader/probe/TinyImageProbe.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/probe/TinyImageProbe.mjs +626 -0
- package/dist/v1/libs/html/loader/utils/TinyBufferReader.cjs +277 -0
- package/dist/v1/libs/html/loader/utils/TinyBufferReader.d.mts +118 -0
- package/dist/v1/libs/html/loader/utils/TinyBufferReader.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/utils/TinyBufferReader.mjs +247 -0
- package/dist/v1/libs/html/loader/utils/TinyIsoBox.cjs +128 -0
- package/dist/v1/libs/html/loader/utils/TinyIsoBox.d.mts +49 -0
- package/dist/v1/libs/html/loader/utils/TinyIsoBox.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/utils/TinyIsoBox.mjs +116 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaCache.cjs +796 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaCache.d.mts +354 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaCache.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaCache.mjs +734 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaMirror.cjs +302 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaMirror.d.mts +256 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaMirror.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaMirror.mjs +280 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaProgress.cjs +194 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaProgress.d.mts +138 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaProgress.d.mts.map +1 -0
- package/dist/v1/libs/html/loader/utils/TinyMediaProgress.mjs +175 -0
- package/dist/v1/libs/tools/TinyPkgExportValidator.cjs +359 -55
- package/dist/v1/libs/tools/TinyPkgExportValidator.d.mts +64 -6
- package/dist/v1/libs/tools/TinyPkgExportValidator.d.mts.map +1 -1
- package/dist/v1/libs/tools/TinyPkgExportValidator.mjs +320 -57
- package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.cjs +0 -1
- package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.d.mts +0 -1
- package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.d.mts.map +1 -1
- package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.mjs +0 -1
- package/dist/v1/webTemplates/bootstrap/5.3/scss/background/_background-grid.scss +15 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/components/buttons/_buttons-contrast.scss +208 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/components/buttons/_buttons-ghost.scss +89 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/components/icon/_cube-icon.scss +65 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/components/icon/_sphere-pulse.scss +39 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_badges.scss +90 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_bootstrap-overrides.scss +495 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_buttons.scss +390 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_colors.scss +213 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_effects.scss +153 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_forms-tones.scss +69 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_forms.scss +323 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_global.scss +31 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_links.scss +47 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_tables.scss +25 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_theme.scss +21 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_tokens.scss +161 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_utilities.scss +81 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_variables.scss +63 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_form-tone-mixins.scss +236 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_sizing.scss +68 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_tone-mixins.scss +392 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_typography.scss +112 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/background/_tiny_gradient_1.scss +131 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/colors/_pink.scss +47 -0
- package/docs/v1/Exports.md +33 -2
- package/docs/v1/README.md +9 -0
- package/docs/v1/basics/youtube.md +204 -0
- package/docs/v1/libs/html/loader/TinyFetchLoader.md +114 -0
- package/docs/v1/libs/html/loader/TinyImageLoader.md +141 -0
- package/docs/v1/libs/html/loader/TinyMediaCache.md +166 -0
- package/docs/v1/libs/html/loader/TinyMediaLoader.md +300 -0
- package/docs/v1/libs/tools/TinyPkgExportValidator.md +55 -0
- package/docs/v1/webTemplates/bootstrap/5.3/DeepStrap.md +177 -0
- package/package.json +244 -8
- /package/dist/v1/webTemplates/bootstrap/5.3/scss/input/{Placeholder-Colors-Fix.scss → _placeholder-colors-fix.scss} +0 -0
- /package/dist/v1/webTemplates/bootstrap/5.3/scss/spacing/{Spacing-Plus.scss → _spacing-plus.scss} +0 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared memory cache for media blobs and object URLs.
|
|
3
|
+
* Instances are tracked statically so a blob survives until the last cache
|
|
4
|
+
* releases it, which prevents revoking an object URL that another part of the
|
|
5
|
+
* application is still rendering.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Cleanup strategy applied when an entry reaches zero references.
|
|
9
|
+
* @typedef {'auto'|'manual'|'ttl'} MediaCacheStrategy
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} MediaCacheOptions
|
|
13
|
+
* @property {number} [maxItems] - Maximum number of entries (Infinity by default).
|
|
14
|
+
* @property {number} [maxBytes] - Maximum cache size in bytes (Infinity by default).
|
|
15
|
+
* @property {number} [ttl] - Entry lifetime in milliseconds (0 = never expires).
|
|
16
|
+
* @property {MediaCacheStrategy} [strategy] - Cleanup strategy at zero references.
|
|
17
|
+
* @property {boolean} [ignoreSearch] - Whether the query string is ignored when building keys.
|
|
18
|
+
* @property {number} [sweepInterval] - Interval in milliseconds for active TTL sweeping (0 = disabled).
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} MediaCacheEntry
|
|
22
|
+
* @property {string} key - The normalized cache key.
|
|
23
|
+
* @property {string} url - The original URL.
|
|
24
|
+
* @property {Blob} blob - The cached blob.
|
|
25
|
+
* @property {string} objectUrl - The shared object URL.
|
|
26
|
+
* @property {number} size - The blob size in bytes.
|
|
27
|
+
* @property {string} type - The blob MIME type.
|
|
28
|
+
* @property {number} refs - The number of active consumers.
|
|
29
|
+
* @property {number} createdAt - The epoch timestamp when the entry was created.
|
|
30
|
+
* @property {number} lastAccess - The epoch timestamp of the last access.
|
|
31
|
+
* @property {number} expiresAt - The epoch timestamp when the entry expires (0 = never).
|
|
32
|
+
* @property {boolean} pinned - Whether the entry is protected from eviction.
|
|
33
|
+
* @property {string|null} parent - The key of the entry that owns this one, or null.
|
|
34
|
+
* @property {Record<string, any>} metadata - The probe metadata captured with the blob.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} MediaCacheStats
|
|
38
|
+
* @property {number} size - The number of entries in this cache.
|
|
39
|
+
* @property {number} bytes - The total size in bytes held by this cache.
|
|
40
|
+
* @property {number} refs - The total number of active references.
|
|
41
|
+
* @property {number} hits - The number of successful lookups.
|
|
42
|
+
* @property {number} misses - The number of failed lookups.
|
|
43
|
+
* @property {number} evictions - The number of evicted entries.
|
|
44
|
+
* @property {number} links - The number of entries that currently own at least one child.
|
|
45
|
+
* @property {number} instances - The number of live cache instances.
|
|
46
|
+
* @property {number} blobs - The number of blobs in the shared registry.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} MediaCacheRecord
|
|
50
|
+
* @property {string} objectUrl - The shared object URL.
|
|
51
|
+
* @property {number} refs - The number of cache instances holding the key.
|
|
52
|
+
* @property {number} size - The blob size in bytes.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* Checks whether a value is a plain object (not an array, not a class instance).
|
|
56
|
+
* @param {unknown} value - The value to test.
|
|
57
|
+
* @returns {boolean} True when the value is a plain object.
|
|
58
|
+
*/
|
|
59
|
+
function isPlainObject(value) {
|
|
60
|
+
if (value === null || typeof value !== 'object')
|
|
61
|
+
return false;
|
|
62
|
+
const proto = Object.getPrototypeOf(value);
|
|
63
|
+
return proto === Object.prototype || proto === null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Memory cache shared by every media loader.
|
|
67
|
+
*/
|
|
68
|
+
class TinyMediaCache {
|
|
69
|
+
/**
|
|
70
|
+
* Cleanup strategies applied when an entry reaches zero references.
|
|
71
|
+
*/
|
|
72
|
+
static Strategy = Object.freeze({
|
|
73
|
+
AUTO: 'auto',
|
|
74
|
+
MANUAL: 'manual',
|
|
75
|
+
TTL: 'ttl',
|
|
76
|
+
});
|
|
77
|
+
/** @type {Set<TinyMediaCache>} */
|
|
78
|
+
static #instances = new Set();
|
|
79
|
+
/** @type {Map<string, MediaCacheRecord>} */
|
|
80
|
+
static #blobs = new Map();
|
|
81
|
+
/** @type {number} */
|
|
82
|
+
#evictions = 0;
|
|
83
|
+
/** @type {Map<string, MediaCacheEntry>} */
|
|
84
|
+
#entries = new Map();
|
|
85
|
+
/** @type {Map<string, Set<string>>} */
|
|
86
|
+
#children = new Map();
|
|
87
|
+
/** @type {number} */
|
|
88
|
+
#maxItems;
|
|
89
|
+
/** @type {number} */
|
|
90
|
+
#maxBytes;
|
|
91
|
+
/** @type {number} */
|
|
92
|
+
#ttl;
|
|
93
|
+
/** @type {MediaCacheStrategy} */
|
|
94
|
+
#strategy;
|
|
95
|
+
/** @type {boolean} */
|
|
96
|
+
#ignoreSearch;
|
|
97
|
+
/** @type {number} */
|
|
98
|
+
#hits = 0;
|
|
99
|
+
/** @type {number} */
|
|
100
|
+
#misses = 0;
|
|
101
|
+
/** @type {ReturnType<typeof setInterval>|null} */
|
|
102
|
+
#timer = null;
|
|
103
|
+
/** @type {boolean} */
|
|
104
|
+
#destroyed = false;
|
|
105
|
+
/**
|
|
106
|
+
* The number of failed lookups recorded by this cache.
|
|
107
|
+
* @returns {number} The current miss count.
|
|
108
|
+
*/
|
|
109
|
+
get misses() {
|
|
110
|
+
return this.#misses;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether the query string is ignored when building cache keys.
|
|
114
|
+
* @returns {boolean} True when the query string is ignored.
|
|
115
|
+
*/
|
|
116
|
+
get ignoreSearch() {
|
|
117
|
+
return this.#ignoreSearch;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The cleanup strategy applied when an entry reaches zero references.
|
|
121
|
+
* @returns {MediaCacheStrategy} The active cleanup strategy used by the cache.
|
|
122
|
+
*/
|
|
123
|
+
get strategy() {
|
|
124
|
+
return this.#strategy;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The entry lifetime in milliseconds.
|
|
128
|
+
* @returns {number} The entry lifetime in milliseconds (0 = never expires).
|
|
129
|
+
*/
|
|
130
|
+
get ttl() {
|
|
131
|
+
return this.#ttl;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The maximum number of entries allowed in this cache.
|
|
135
|
+
* @returns {number} The maximum number of entries.
|
|
136
|
+
*/
|
|
137
|
+
get maxItems() {
|
|
138
|
+
return this.#maxItems;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The maximum size in bytes allowed in this cache.
|
|
142
|
+
* @returns {number} The maximum size in bytes.
|
|
143
|
+
*/
|
|
144
|
+
get maxBytes() {
|
|
145
|
+
return this.#maxBytes;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The number of entries evicted by the limit enforcement.
|
|
149
|
+
* @returns {number} The current eviction count.
|
|
150
|
+
*/
|
|
151
|
+
get evictions() {
|
|
152
|
+
return this.#evictions;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* @param {MediaCacheOptions} [options] - The cache configuration.
|
|
156
|
+
* @throws {TypeError} If `options` is not a plain object.
|
|
157
|
+
* @throws {RangeError} If `maxItems` is not a positive number.
|
|
158
|
+
* @throws {RangeError} If `maxBytes` is not a positive number.
|
|
159
|
+
* @throws {RangeError} If `ttl` is not a positive number.
|
|
160
|
+
* @throws {TypeError} If `strategy` is not a known strategy.
|
|
161
|
+
* @throws {TypeError} If `ignoreSearch` is not a boolean.
|
|
162
|
+
* @throws {RangeError} If `sweepInterval` is not a positive number.
|
|
163
|
+
*/
|
|
164
|
+
constructor(options = {}) {
|
|
165
|
+
if (!isPlainObject(options)) {
|
|
166
|
+
throw new TypeError('The "options" argument must be a plain object.');
|
|
167
|
+
}
|
|
168
|
+
const { maxItems = Infinity, maxBytes = Infinity, ttl = 0, strategy = TinyMediaCache.Strategy.MANUAL, ignoreSearch = false, sweepInterval = 0, } = options;
|
|
169
|
+
if (typeof maxItems !== 'number' || Number.isNaN(maxItems) || maxItems < 1) {
|
|
170
|
+
throw new RangeError('The "maxItems" option must be a positive number.');
|
|
171
|
+
}
|
|
172
|
+
if (maxItems !== Infinity && !Number.isInteger(maxItems)) {
|
|
173
|
+
throw new RangeError('The "maxItems" option must be an integer or Infinity.');
|
|
174
|
+
}
|
|
175
|
+
if (typeof maxBytes !== 'number' || Number.isNaN(maxBytes) || maxBytes < 0) {
|
|
176
|
+
throw new RangeError('The "maxBytes" option must be a positive number.');
|
|
177
|
+
}
|
|
178
|
+
if (typeof ttl !== 'number' || Number.isNaN(ttl) || ttl < 0) {
|
|
179
|
+
throw new RangeError('The "ttl" option must be a positive number.');
|
|
180
|
+
}
|
|
181
|
+
if (!Object.values(TinyMediaCache.Strategy).includes(strategy)) {
|
|
182
|
+
throw new TypeError(`Unknown cache strategy: ${strategy}`);
|
|
183
|
+
}
|
|
184
|
+
if (typeof ignoreSearch !== 'boolean') {
|
|
185
|
+
throw new TypeError('The "ignoreSearch" option must be a boolean.');
|
|
186
|
+
}
|
|
187
|
+
if (typeof sweepInterval !== 'number' || Number.isNaN(sweepInterval) || sweepInterval < 0) {
|
|
188
|
+
throw new RangeError('The "sweepInterval" option must be a positive number.');
|
|
189
|
+
}
|
|
190
|
+
this.#maxItems = maxItems;
|
|
191
|
+
this.#maxBytes = maxBytes;
|
|
192
|
+
this.#ttl = ttl;
|
|
193
|
+
this.#strategy = strategy;
|
|
194
|
+
this.#ignoreSearch = ignoreSearch;
|
|
195
|
+
TinyMediaCache.#instances.add(this);
|
|
196
|
+
if (sweepInterval > 0) {
|
|
197
|
+
this.#timer = setInterval(() => this.prune(), sweepInterval);
|
|
198
|
+
if (typeof this.#timer.unref === 'function') {
|
|
199
|
+
this.#timer.unref();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A read-only snapshot of the shared blob registry.
|
|
205
|
+
* @returns {Array<{ key: string, objectUrl: string, refs: number, size: number }>} One record per cached blob.
|
|
206
|
+
*/
|
|
207
|
+
static get registry() {
|
|
208
|
+
return Array.from(TinyMediaCache.#blobs, ([key, record]) => ({ key, ...record }));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Every live cache instance.
|
|
212
|
+
* @returns {TinyMediaCache[]}
|
|
213
|
+
*/
|
|
214
|
+
static get instances() {
|
|
215
|
+
return Array.from(TinyMediaCache.#instances);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The number of blobs held by the shared registry.
|
|
219
|
+
* @returns {number}
|
|
220
|
+
*/
|
|
221
|
+
static get size() {
|
|
222
|
+
return TinyMediaCache.#blobs.size;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The total number of bytes held by the shared registry.
|
|
226
|
+
* @returns {number}
|
|
227
|
+
*/
|
|
228
|
+
static get bytes() {
|
|
229
|
+
let total = 0;
|
|
230
|
+
for (const record of TinyMediaCache.#blobs.values()) {
|
|
231
|
+
total += record.size;
|
|
232
|
+
}
|
|
233
|
+
return total;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Checks the shared registry without touching any instance.
|
|
237
|
+
* @param {string} url - The media URL.
|
|
238
|
+
* @param {boolean} [ignoreSearch] - Whether the query string is ignored.
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
static has(url, ignoreSearch = false) {
|
|
242
|
+
return TinyMediaCache.#blobs.has(TinyMediaCache.#normalize(url, ignoreSearch));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Revokes every object URL in the shared registry.
|
|
246
|
+
* @returns {void}
|
|
247
|
+
*/
|
|
248
|
+
static clear() {
|
|
249
|
+
for (const record of TinyMediaCache.#blobs.values()) {
|
|
250
|
+
URL.revokeObjectURL(record.objectUrl);
|
|
251
|
+
}
|
|
252
|
+
TinyMediaCache.#blobs.clear();
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Destroys every live cache instance.
|
|
256
|
+
* @returns {void}
|
|
257
|
+
*/
|
|
258
|
+
static destroyAll() {
|
|
259
|
+
for (const instance of Array.from(TinyMediaCache.#instances)) {
|
|
260
|
+
instance.destroy();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The number of entries currently stored in this cache instance.
|
|
265
|
+
* @returns {number} The number of entries in this cache.
|
|
266
|
+
*/
|
|
267
|
+
get size() {
|
|
268
|
+
return this.#entries.size;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The number of children's currently stored in this cache instance.
|
|
272
|
+
* @returns {number} The number of children's in this cache.
|
|
273
|
+
*/
|
|
274
|
+
get links() {
|
|
275
|
+
return this.#children.size;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* @returns {number} The total size in bytes held by this cache.
|
|
279
|
+
*/
|
|
280
|
+
get bytes() {
|
|
281
|
+
let total = 0;
|
|
282
|
+
for (const entry of this.#entries.values()) {
|
|
283
|
+
total += entry.size;
|
|
284
|
+
}
|
|
285
|
+
return total;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Indicates whether the cache instance has been destroyed.
|
|
289
|
+
* @returns {boolean} True if the cache was destroyed, false otherwise.
|
|
290
|
+
*/
|
|
291
|
+
get destroyed() {
|
|
292
|
+
return this.#destroyed;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Returns a snapshot of the cache's internal performance counters.
|
|
296
|
+
* @returns {MediaCacheStats} A snapshot of the cache counters.
|
|
297
|
+
*/
|
|
298
|
+
get stats() {
|
|
299
|
+
let refs = 0;
|
|
300
|
+
for (const entry of this.#entries.values()) {
|
|
301
|
+
refs += entry.refs;
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
size: this.#entries.size,
|
|
305
|
+
bytes: this.bytes,
|
|
306
|
+
refs,
|
|
307
|
+
hits: this.#hits,
|
|
308
|
+
misses: this.#misses,
|
|
309
|
+
evictions: this.#evictions,
|
|
310
|
+
links: this.#children.size,
|
|
311
|
+
instances: TinyMediaCache.#instances.size,
|
|
312
|
+
blobs: TinyMediaCache.#blobs.size,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* A read-only snapshot of every entry held by this cache.
|
|
317
|
+
* @returns {MediaCacheEntry[]} A shallow copy of the entries.
|
|
318
|
+
*/
|
|
319
|
+
entries() {
|
|
320
|
+
this.#assertAlive();
|
|
321
|
+
return Array.from(this.#entries.values(), (entry) => ({
|
|
322
|
+
...entry,
|
|
323
|
+
metadata: { ...entry.metadata },
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Stores a blob and returns its entry.
|
|
328
|
+
* @param {string} url - The media URL.
|
|
329
|
+
* @param {Blob} blob - The blob to cache.
|
|
330
|
+
* @param {boolean} [pinned] - Whether the entry is protected from eviction.
|
|
331
|
+
* @param {Record<string, any>} [metadata] - The probe metadata captured with the blob.
|
|
332
|
+
* @param {string|null} [parent] - The key of the entry that owns this one.
|
|
333
|
+
* @returns {MediaCacheEntry} The stored entry.
|
|
334
|
+
* @throws {TypeError} If `blob` is not a Blob.
|
|
335
|
+
* @throws {TypeError} If `parent` is neither a string nor null.
|
|
336
|
+
* @throws {Error} If the cache was destroyed.
|
|
337
|
+
*/
|
|
338
|
+
set(url, blob, pinned = false, metadata = {}, parent = null) {
|
|
339
|
+
this.#assertAlive();
|
|
340
|
+
if (!(blob instanceof Blob)) {
|
|
341
|
+
throw new TypeError('The "blob" argument must be a Blob.');
|
|
342
|
+
}
|
|
343
|
+
if (typeof pinned !== 'boolean') {
|
|
344
|
+
throw new TypeError('The "pinned" argument must be a boolean.');
|
|
345
|
+
}
|
|
346
|
+
if (!isPlainObject(metadata)) {
|
|
347
|
+
throw new TypeError('The "metadata" argument must be a plain object.');
|
|
348
|
+
}
|
|
349
|
+
if (parent !== null && typeof parent !== 'string') {
|
|
350
|
+
throw new TypeError('The "parent" argument must be a string or null.');
|
|
351
|
+
}
|
|
352
|
+
const key = this.#key(url);
|
|
353
|
+
this.#discard(key, false);
|
|
354
|
+
const record = TinyMediaCache.#acquire(key, blob);
|
|
355
|
+
const now = Date.now();
|
|
356
|
+
/** @type {MediaCacheEntry} */
|
|
357
|
+
const entry = {
|
|
358
|
+
key,
|
|
359
|
+
url,
|
|
360
|
+
blob,
|
|
361
|
+
objectUrl: record.objectUrl,
|
|
362
|
+
size: blob.size,
|
|
363
|
+
type: blob.type,
|
|
364
|
+
refs: 0,
|
|
365
|
+
createdAt: now,
|
|
366
|
+
lastAccess: now,
|
|
367
|
+
expiresAt: this.#ttl > 0 ? now + this.#ttl : 0,
|
|
368
|
+
pinned: Boolean(pinned),
|
|
369
|
+
parent,
|
|
370
|
+
metadata: { ...metadata },
|
|
371
|
+
};
|
|
372
|
+
this.#entries.set(key, entry);
|
|
373
|
+
if (parent !== null) {
|
|
374
|
+
this.#link(parent, key);
|
|
375
|
+
}
|
|
376
|
+
this.#enforceLimit();
|
|
377
|
+
return entry;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Reads an entry without changing its reference count.
|
|
381
|
+
* @param {string} url - The media URL.
|
|
382
|
+
* @returns {MediaCacheEntry|null}
|
|
383
|
+
*/
|
|
384
|
+
get(url) {
|
|
385
|
+
this.#assertAlive();
|
|
386
|
+
const key = this.#key(url);
|
|
387
|
+
const entry = this.#entries.get(key);
|
|
388
|
+
if (!entry) {
|
|
389
|
+
this.#misses += 1;
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
if (this.#isExpired(entry)) {
|
|
393
|
+
this.delete(key);
|
|
394
|
+
this.#misses += 1;
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
entry.lastAccess = Date.now();
|
|
398
|
+
this.#hits += 1;
|
|
399
|
+
return entry;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Checks whether a live entry exists for the given URL.
|
|
403
|
+
* @param {string} url - The media URL.
|
|
404
|
+
* @returns {boolean} True when a non-expired entry exists.
|
|
405
|
+
*/
|
|
406
|
+
has(url) {
|
|
407
|
+
this.#assertAlive();
|
|
408
|
+
const entry = this.#entries.get(this.#key(url));
|
|
409
|
+
return entry !== undefined && !this.#isExpired(entry);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Increments the reference count of an entry.
|
|
413
|
+
* @param {string} url - The media URL.
|
|
414
|
+
* @returns {MediaCacheEntry|null} The acquired entry, or null when missing.
|
|
415
|
+
*/
|
|
416
|
+
acquire(url) {
|
|
417
|
+
this.#assertAlive();
|
|
418
|
+
const entry = this.get(url);
|
|
419
|
+
if (!entry) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
entry.refs += 1;
|
|
423
|
+
return entry;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Decrements the reference count and applies the cleanup strategy.
|
|
427
|
+
* @param {string} url - The media URL.
|
|
428
|
+
* @returns {boolean} True when the entry was released.
|
|
429
|
+
*/
|
|
430
|
+
release(url) {
|
|
431
|
+
this.#assertAlive();
|
|
432
|
+
const key = this.#key(url);
|
|
433
|
+
const entry = this.#entries.get(key);
|
|
434
|
+
if (!entry) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
438
|
+
if (entry.refs === 0 && this.#strategy === TinyMediaCache.Strategy.AUTO) {
|
|
439
|
+
this.delete(key);
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Removes an entry and releases its share of the shared registry. When the
|
|
445
|
+
* entry is the last child of a parent, the parent is removed as well.
|
|
446
|
+
* @param {string} url - The media URL.
|
|
447
|
+
* @returns {boolean} True when an entry was removed.
|
|
448
|
+
*/
|
|
449
|
+
delete(url) {
|
|
450
|
+
this.#assertAlive();
|
|
451
|
+
return this.#discard(this.#key(url), true);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* The keys of the entries owned by the given key.
|
|
455
|
+
* @param {string} url - The media URL of the owner.
|
|
456
|
+
* @returns {string[]} The normalized keys of the children, in insertion order.
|
|
457
|
+
*/
|
|
458
|
+
children(url) {
|
|
459
|
+
return Array.from(this.#children.get(this.#key(url)) ?? []);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Removes an entry, releases its share of the shared registry and, when it
|
|
463
|
+
* was the last child, removes the parent that owns it.
|
|
464
|
+
* @param {string} key - The normalized cache key.
|
|
465
|
+
* @param {boolean} cascade - Whether removing the last child also removes the parent.
|
|
466
|
+
* @returns {boolean} True when an entry was removed.
|
|
467
|
+
* @throws {TypeError} If `key` is not a string or `cascade` is not a boolean.
|
|
468
|
+
*/
|
|
469
|
+
#discard(key, cascade) {
|
|
470
|
+
if (typeof key !== 'string') {
|
|
471
|
+
throw new TypeError('The "key" argument must be a string.');
|
|
472
|
+
}
|
|
473
|
+
if (typeof cascade !== 'boolean') {
|
|
474
|
+
throw new TypeError('The "cascade" argument must be a boolean.');
|
|
475
|
+
}
|
|
476
|
+
const entry = this.#entries.get(key);
|
|
477
|
+
if (!entry) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
this.#entries.delete(key);
|
|
481
|
+
TinyMediaCache.#release(key);
|
|
482
|
+
this.#children.delete(key);
|
|
483
|
+
if (entry.parent === null) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
const siblings = this.#children.get(entry.parent);
|
|
487
|
+
if (!siblings) {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
siblings.delete(key);
|
|
491
|
+
if (siblings.size > 0) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
this.#children.delete(entry.parent);
|
|
495
|
+
const owner = this.#entries.get(entry.parent);
|
|
496
|
+
if (cascade && owner && owner.refs === 0) {
|
|
497
|
+
this.#discard(entry.parent, true);
|
|
498
|
+
}
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Registers a child entry under the entry that owns it.
|
|
503
|
+
* @param {string} parent - The normalized key of the owner.
|
|
504
|
+
* @param {string} child - The normalized key of the child.
|
|
505
|
+
* @returns {void}
|
|
506
|
+
* @throws {TypeError} If `parent` or `child` is not a string.
|
|
507
|
+
*/
|
|
508
|
+
#link(parent, child) {
|
|
509
|
+
if (typeof parent !== 'string') {
|
|
510
|
+
throw new TypeError('The "parent" argument must be a string.');
|
|
511
|
+
}
|
|
512
|
+
if (typeof child !== 'string') {
|
|
513
|
+
throw new TypeError('The "child" argument must be a string.');
|
|
514
|
+
}
|
|
515
|
+
let children = this.#children.get(parent);
|
|
516
|
+
if (!children) {
|
|
517
|
+
children = new Set();
|
|
518
|
+
this.#children.set(parent, children);
|
|
519
|
+
}
|
|
520
|
+
children.add(child);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Reads an entry without changing its reference count and without touching
|
|
524
|
+
* the hit/miss counters. Expired entries are reported as missing but are not
|
|
525
|
+
* removed, so a read-only probe never mutates the cache.
|
|
526
|
+
* @param {string} url - The media URL.
|
|
527
|
+
* @returns {MediaCacheEntry|null} The live entry, or null when absent or expired.
|
|
528
|
+
* @throws {TypeError} If `url` is not a string.
|
|
529
|
+
* @throws {Error} If the cache was destroyed.
|
|
530
|
+
*/
|
|
531
|
+
peek(url) {
|
|
532
|
+
this.#assertAlive();
|
|
533
|
+
if (typeof url !== 'string') {
|
|
534
|
+
throw new TypeError('The "url" argument must be a string.');
|
|
535
|
+
}
|
|
536
|
+
const entry = this.#entries.get(this.#key(url));
|
|
537
|
+
if (!entry || this.#isExpired(entry)) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
return entry;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Pins an entry so the limit enforcement skips it.
|
|
544
|
+
* @param {string} url - The media URL.
|
|
545
|
+
* @param {boolean} [value] - The new pinned state.
|
|
546
|
+
* @returns {boolean} True when the entry exists.
|
|
547
|
+
*/
|
|
548
|
+
pin(url, value = true) {
|
|
549
|
+
this.#assertAlive();
|
|
550
|
+
if (typeof value !== 'boolean') {
|
|
551
|
+
throw new TypeError('The "value" argument must be a boolean.');
|
|
552
|
+
}
|
|
553
|
+
const entry = this.#entries.get(this.#key(url));
|
|
554
|
+
if (!entry) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
entry.pinned = value;
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Removes every expired entry.
|
|
562
|
+
* @returns {number} The number of removed entries.
|
|
563
|
+
*/
|
|
564
|
+
prune() {
|
|
565
|
+
this.#assertAlive();
|
|
566
|
+
const expired = [];
|
|
567
|
+
for (const [key, entry] of this.#entries) {
|
|
568
|
+
if (this.#isExpired(entry)) {
|
|
569
|
+
expired.push(key);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
let removed = 0;
|
|
573
|
+
for (const key of expired) {
|
|
574
|
+
if (this.#discard(key, true)) {
|
|
575
|
+
removed += 1;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return removed;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Removes every entry, regardless of its reference count.
|
|
582
|
+
* @returns {void}
|
|
583
|
+
*/
|
|
584
|
+
clear() {
|
|
585
|
+
this.#assertAlive();
|
|
586
|
+
for (const key of Array.from(this.#entries.keys())) {
|
|
587
|
+
this.#entries.delete(key);
|
|
588
|
+
TinyMediaCache.#release(key);
|
|
589
|
+
}
|
|
590
|
+
this.#children.clear();
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Detaches this instance from the shared registry.
|
|
594
|
+
* @returns {void}
|
|
595
|
+
*/
|
|
596
|
+
destroy() {
|
|
597
|
+
if (this.#destroyed) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (this.#timer) {
|
|
601
|
+
clearInterval(this.#timer);
|
|
602
|
+
this.#timer = null;
|
|
603
|
+
}
|
|
604
|
+
this.clear();
|
|
605
|
+
TinyMediaCache.#instances.delete(this);
|
|
606
|
+
this.#destroyed = true;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Evicts the least recently used entries until both the quantity limit
|
|
610
|
+
* and the byte limit are respected.
|
|
611
|
+
* @returns {void}
|
|
612
|
+
*/
|
|
613
|
+
#enforceLimit() {
|
|
614
|
+
if (this.#entries.size <= this.#maxItems && this.bytes <= this.#maxBytes) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const candidates = Array.from(this.#entries.values())
|
|
618
|
+
.filter((entry) => !entry.pinned && entry.refs === 0 && !this.#children.has(entry.key))
|
|
619
|
+
.sort((a, b) => a.lastAccess - b.lastAccess);
|
|
620
|
+
let currentBytes = this.bytes;
|
|
621
|
+
let currentItems = this.#entries.size;
|
|
622
|
+
for (const entry of candidates) {
|
|
623
|
+
if (currentItems <= this.#maxItems && currentBytes <= this.#maxBytes) {
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
this.delete(entry.key);
|
|
627
|
+
this.#evictions += 1;
|
|
628
|
+
currentBytes -= entry.size;
|
|
629
|
+
currentItems -= 1;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Checks whether an entry has passed its expiration time.
|
|
634
|
+
* @param {MediaCacheEntry} entry - The entry to test.
|
|
635
|
+
* @returns {boolean} True when the entry has expired.
|
|
636
|
+
* @throws {TypeError} If `entry` is not an object.
|
|
637
|
+
*/
|
|
638
|
+
#isExpired(entry) {
|
|
639
|
+
if (entry === null || typeof entry !== 'object') {
|
|
640
|
+
throw new TypeError('The "entry" argument must be a MediaCacheEntry.');
|
|
641
|
+
}
|
|
642
|
+
return entry.expiresAt > 0 && Date.now() >= entry.expiresAt;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Builds the normalized cache key for a URL.
|
|
646
|
+
* @param {string} url - The media URL.
|
|
647
|
+
* @returns {string} The normalized cache key.
|
|
648
|
+
*/
|
|
649
|
+
#key(url) {
|
|
650
|
+
return TinyMediaCache.#normalize(url, this.#ignoreSearch);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Ensures the cache has not been destroyed before performing an operation.
|
|
654
|
+
* @returns {void}
|
|
655
|
+
* @throws {Error} If the cache was destroyed.
|
|
656
|
+
*/
|
|
657
|
+
#assertAlive() {
|
|
658
|
+
if (this.#destroyed) {
|
|
659
|
+
throw new Error('Cannot use a destroyed media cache.');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Normalizes a URL into a cache key.
|
|
664
|
+
* @param {string} url - The media URL.
|
|
665
|
+
* @param {boolean} ignoreSearch - Whether the query string is ignored.
|
|
666
|
+
* @returns {string} The normalized cache key.
|
|
667
|
+
* @throws {TypeError} If `url` is not a string.
|
|
668
|
+
* @throws {TypeError} If `ignoreSearch` is not a boolean.
|
|
669
|
+
*/
|
|
670
|
+
static #normalize(url, ignoreSearch) {
|
|
671
|
+
if (typeof url !== 'string') {
|
|
672
|
+
throw new TypeError('The "url" argument must be a string.');
|
|
673
|
+
}
|
|
674
|
+
if (typeof ignoreSearch !== 'boolean') {
|
|
675
|
+
throw new TypeError('The "ignoreSearch" argument must be a boolean.');
|
|
676
|
+
}
|
|
677
|
+
if (!ignoreSearch) {
|
|
678
|
+
return url;
|
|
679
|
+
}
|
|
680
|
+
const clean = url.split('#')[0];
|
|
681
|
+
const query = clean.indexOf('?');
|
|
682
|
+
return query === -1 ? clean : clean.slice(0, query);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Registers a blob in the shared registry and returns its record.
|
|
686
|
+
* @param {string} key - The normalized cache key.
|
|
687
|
+
* @param {Blob} blob - The blob to register.
|
|
688
|
+
* @returns {MediaCacheRecord} The shared registry record.
|
|
689
|
+
* @throws {TypeError} If `key` is not a string or `blob` is not a Blob.
|
|
690
|
+
*/
|
|
691
|
+
static #acquire(key, blob) {
|
|
692
|
+
if (typeof key !== 'string') {
|
|
693
|
+
throw new TypeError('The "key" argument must be a string.');
|
|
694
|
+
}
|
|
695
|
+
if (!(blob instanceof Blob)) {
|
|
696
|
+
throw new TypeError('The "blob" argument must be a Blob.');
|
|
697
|
+
}
|
|
698
|
+
const existing = TinyMediaCache.#blobs.get(key);
|
|
699
|
+
if (existing) {
|
|
700
|
+
existing.refs += 1;
|
|
701
|
+
return existing;
|
|
702
|
+
}
|
|
703
|
+
/** @type {MediaCacheRecord} */
|
|
704
|
+
const record = {
|
|
705
|
+
objectUrl: URL.createObjectURL(blob),
|
|
706
|
+
refs: 1,
|
|
707
|
+
size: blob.size,
|
|
708
|
+
};
|
|
709
|
+
TinyMediaCache.#blobs.set(key, record);
|
|
710
|
+
return record;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Releases a reference in the shared registry and revokes the object URL when unused.
|
|
714
|
+
* @param {string} key - The normalized cache key.
|
|
715
|
+
* @returns {void}
|
|
716
|
+
* @throws {TypeError} If `key` is not a string.
|
|
717
|
+
*/
|
|
718
|
+
static #release(key) {
|
|
719
|
+
if (typeof key !== 'string') {
|
|
720
|
+
throw new TypeError('The "key" argument must be a string.');
|
|
721
|
+
}
|
|
722
|
+
const record = TinyMediaCache.#blobs.get(key);
|
|
723
|
+
if (!record) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
record.refs -= 1;
|
|
727
|
+
if (record.refs > 0) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
URL.revokeObjectURL(record.objectUrl);
|
|
731
|
+
TinyMediaCache.#blobs.delete(key);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
export default TinyMediaCache;
|