@banou/ponyfill 0.0.3 → 0.0.5
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/README.md +12 -4
- package/build/index.cjs +0 -5
- package/build/index.d.ts +11 -6
- package/build/index.js +2 -2
- package/build/storage.cjs +64 -120
- package/build/storage.d.ts +47 -140
- package/build/storage.js +65 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,10 +58,18 @@ That is not hypothetical. It is why four of ripple's storage eviction tests sat
|
|
|
58
58
|
they filled the origin to provoke that condition, and 3.5 GB of padding left the free figure
|
|
59
59
|
identical to the byte.
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
**The behaviour picked is Firefox's: a ceiling does not rise because you put something under it.**
|
|
62
|
+
`quota` is reported as the lowest the platform has stated for this origin, which needs no probe and
|
|
63
|
+
no user agent sniffing, and invents nothing: the platform really did say the origin could hold that
|
|
64
|
+
much. Two ordinary things are broken by the rising version, and both were seen in ripple:
|
|
65
|
+
`usage / quota` as a gauge never fills, and `quota - usage < floor` never fires, so nothing can
|
|
66
|
+
detect pressure at all.
|
|
67
|
+
|
|
68
|
+
The cost is deliberate and in the safe direction. On Chromium, once bytes are written the reported
|
|
69
|
+
headroom is smaller than what could really be written, so a caller reclaims cache slightly early.
|
|
70
|
+
Cache is the thing that can be fetched again; a caller that never reclaims because it never sees
|
|
71
|
+
pressure is the failure this replaces. The ceiling still follows the platform DOWNWARD, and is never
|
|
72
|
+
reported below the bytes already held, so `quota - usage` reaches zero and never goes negative.
|
|
65
73
|
|
|
66
74
|
## Adding to it
|
|
67
75
|
|
package/build/index.cjs
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_storage = require("./storage.cjs");
|
|
3
|
-
exports.QUOTA_CEILING = require_storage.QUOTA_CEILING;
|
|
4
|
-
exports.correctedUsage = require_storage.correctedUsage;
|
|
5
|
-
exports.isUsageUnderReported = require_storage.isUsageUnderReported;
|
|
6
|
-
exports.measureDirectoryBytes = require_storage.measureDirectoryBytes;
|
|
7
|
-
exports.measureQuotaCeiling = require_storage.measureQuotaCeiling;
|
|
8
3
|
exports.storage = require_storage.storage;
|
package/build/index.d.ts
CHANGED
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
* import { storage } from '@banou/ponyfill'
|
|
8
8
|
* const { usage, quota } = await storage.estimate()
|
|
9
9
|
*
|
|
10
|
-
* That constraint is the whole design. A polyfill that patched
|
|
11
|
-
* every other script on the page sees, including code that was
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* That constraint is the whole design, and it cuts both ways. A polyfill that patched
|
|
11
|
+
* `navigator.storage` would change what every other script on the page sees, including code that was
|
|
12
|
+
* correct against the real behaviour. And an export that is not a platform name is not a ponyfill of
|
|
13
|
+
* anything: it is a utility library wearing the word. So the surface here is exactly the platform's
|
|
14
|
+
* surface, and a helper only becomes public if the platform has one by that name.
|
|
15
|
+
*
|
|
16
|
+
* Everything else lives behind it. The walk that corrects `estimate().usage`, the bounds on that
|
|
17
|
+
* walk, and the reconciliation between the measured and reported figures are all internal, because
|
|
18
|
+
* `navigator.storage` has no such members and neither should this.
|
|
14
19
|
*
|
|
15
20
|
* ## What belongs in here
|
|
16
21
|
*
|
|
@@ -22,5 +27,5 @@
|
|
|
22
27
|
* Every module here states what was measured, on what, and when. A workaround with no measurement
|
|
23
28
|
* behind it is a guess that outlives the bug it was written for.
|
|
24
29
|
*/
|
|
25
|
-
export {
|
|
26
|
-
export type {
|
|
30
|
+
export { storage } from './storage';
|
|
31
|
+
export type { StorageEstimate } from './storage';
|
package/build/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { storage } from "./storage.js";
|
|
2
|
+
export { storage };
|
package/build/storage.cjs
CHANGED
|
@@ -1,58 +1,39 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
//#region src/storage.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Never walk forever: a cycle is impossible in the origin private file system, but a pathological
|
|
5
|
+
* tree is not, and neither is a directory with a hundred thousand entries in it.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* {@link measureQuotaCeiling} is what answers the question for the origin actually in front of you.
|
|
9
|
-
*/
|
|
10
|
-
var QUOTA_CEILING = {
|
|
11
|
-
chromium: {
|
|
12
|
-
ceiling: "elastic",
|
|
13
|
-
measured: "2026-09-03, Chrome 152.0.7977.64, 2.7 TiB free",
|
|
14
|
-
note: "quota rose 10.737 GB to 12.353 GB across 1.615 GB written, leaving quota - usage at 10,737,418,240 bytes after every write, unmoved to the byte"
|
|
15
|
-
},
|
|
16
|
-
firefox: {
|
|
17
|
-
ceiling: "fixed",
|
|
18
|
-
measured: "2026-09-03, Playwright firefox 1532, same machine and origin",
|
|
19
|
-
note: "quota held at 10,737,418,240 while the headroom fell by the 1,613,063,025 bytes written, byte for byte"
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
var isDirectory = (handle) => typeof handle.values === "function";
|
|
23
|
-
/**
|
|
24
|
-
* Never walk forever: a cycle is impossible in OPFS, but a pathological tree is not worth the tick.
|
|
25
|
-
*
|
|
26
|
-
* These are bounds, not correctness: hitting either returns what was counted so far rather than
|
|
27
|
-
* failing, which keeps the answer a floor in the same direction as everything else here.
|
|
7
|
+
* Bounds rather than correctness. Hitting either returns what was counted so far instead of failing,
|
|
8
|
+
* which keeps the answer a floor in the same direction as everything else here.
|
|
28
9
|
*/
|
|
29
10
|
var MAX_DEPTH = 8;
|
|
30
11
|
var MAX_ENTRIES = 2e4;
|
|
12
|
+
var isDirectory = (handle) => typeof handle.values === "function";
|
|
31
13
|
/**
|
|
32
|
-
* Every byte under a directory,
|
|
33
|
-
* be done at all.
|
|
14
|
+
* Every byte under a directory, or `null` when the walk could not be done at all.
|
|
34
15
|
*
|
|
35
|
-
* The null is a distinct third answer and
|
|
36
|
-
* file system was unreachable" lead to opposite decisions, and collapsing them to
|
|
37
|
-
*
|
|
16
|
+
* The null is a distinct third answer and the correction below depends on it: "the origin holds
|
|
17
|
+
* nothing" and "the file system was unreachable" lead to opposite decisions, and collapsing them to
|
|
18
|
+
* 0 would report an empty origin for one that simply could not be read.
|
|
38
19
|
*
|
|
39
|
-
* Counts a file's REPORTED SIZE, which for
|
|
40
|
-
* written. That is the same accounting the quota system uses, which is the point: a one byte
|
|
41
|
-
* gigabyte into a file is charged a gigabyte, and a measurement
|
|
42
|
-
* no more useful than the figure it replaces.
|
|
20
|
+
* Counts a file's REPORTED SIZE, which for this file system is its extent rather than how much of it
|
|
21
|
+
* has been written. That is the same accounting the quota system uses, which is the point: a one byte
|
|
22
|
+
* write a gigabyte into a file is charged a gigabyte, and a measurement that disagreed with the
|
|
23
|
+
* charge would be no more useful than the figure it replaces.
|
|
43
24
|
*
|
|
44
|
-
* An unreadable ENTRY is skipped rather than fatal. A file
|
|
45
|
-
* sync access handle for cannot be opened
|
|
25
|
+
* An unreadable ENTRY is skipped rather than fatal. A file something else currently holds an
|
|
26
|
+
* exclusive sync access handle for cannot be opened, and `getFile()` on it throws rather than
|
|
46
27
|
* waiting, so a walk that gave up on the first of those would return nothing for exactly the origins
|
|
47
28
|
* holding the most. The answer is therefore a floor: short by the files open right now, never long.
|
|
48
29
|
*/
|
|
49
|
-
var
|
|
30
|
+
var walkBytes = async (directory) => {
|
|
50
31
|
let total = 0;
|
|
51
32
|
let seen = 0;
|
|
52
33
|
const visit = async (handle, depth) => {
|
|
53
|
-
if (depth >
|
|
34
|
+
if (depth > MAX_DEPTH) return;
|
|
54
35
|
for await (const child of handle.values()) {
|
|
55
|
-
if (++seen >
|
|
36
|
+
if (++seen > MAX_ENTRIES) return;
|
|
56
37
|
if (isDirectory(child)) {
|
|
57
38
|
await visit(child, depth + 1);
|
|
58
39
|
continue;
|
|
@@ -69,61 +50,73 @@ var measureDirectoryBytes = async (directory, { maxDepth = 8, maxEntries = MAX_E
|
|
|
69
50
|
}
|
|
70
51
|
};
|
|
71
52
|
/**
|
|
72
|
-
* The usage
|
|
53
|
+
* The usage to report, given what the platform said and what the walk found.
|
|
73
54
|
*
|
|
74
|
-
* The
|
|
75
|
-
*
|
|
76
|
-
* raise the answer.
|
|
77
|
-
*
|
|
78
|
-
* `usageDetails.fileSystem` is subtracted out before the walk is added when the browser volunteers
|
|
79
|
-
* it, because everything else the browser counts (IndexedDB, caches, service worker registrations)
|
|
80
|
-
* it counts correctly and the walk cannot see any of it. Without that split, an origin holding real
|
|
81
|
-
* IndexedDB data would have it silently dropped from the total.
|
|
82
|
-
*
|
|
83
|
-
* Pure, so the arithmetic is testable without a browser: this is the part that can be wrong in a way
|
|
84
|
-
* no integration test would notice.
|
|
55
|
+
* The walk can only ever RAISE the answer: a browser over-reporting has never been observed, while
|
|
56
|
+
* one under-reporting by six orders of magnitude has.
|
|
85
57
|
*/
|
|
86
|
-
var
|
|
58
|
+
var reconcile = (estimate, walked) => {
|
|
87
59
|
const reported = estimate.usage;
|
|
88
|
-
if (
|
|
89
|
-
if (reported === void 0) return
|
|
60
|
+
if (walked === null || !Number.isFinite(walked) || walked < 0) return reported;
|
|
61
|
+
if (reported === void 0) return walked;
|
|
90
62
|
const fileSystem = estimate.usageDetails?.fileSystem;
|
|
91
63
|
if (typeof fileSystem === "number") {
|
|
92
64
|
const other = Math.max(0, reported - fileSystem);
|
|
93
|
-
return Math.max(reported,
|
|
65
|
+
return Math.max(reported, walked + other);
|
|
94
66
|
}
|
|
95
|
-
return Math.max(reported,
|
|
67
|
+
return Math.max(reported, walked);
|
|
96
68
|
};
|
|
97
|
-
/** True when the browser's own figure is too far below the measured one to be believed. */
|
|
98
|
-
var isUsageUnderReported = (estimate, walkedBytes) => walkedBytes !== null && walkedBytes > 0 && (estimate.usage ?? 0) < walkedBytes / 2;
|
|
99
69
|
/**
|
|
100
|
-
*
|
|
70
|
+
* The narrowest quota this origin has reported, which is the one that gets reported back.
|
|
71
|
+
*
|
|
72
|
+
* THE DIVERGENCE, AND THE PICK. `quota - usage` already means the same on both engines: bytes you
|
|
73
|
+
* can still write. What diverges is `quota` as a TOTAL, and Chromium never states one. It states
|
|
74
|
+
* `usage + headroom`, recomputed on every read, so the number grows as you fill the origin: measured
|
|
75
|
+
* 10.737 GB rising to 12.353 GB across 1.615 GB written. Firefox states a real total and holds it.
|
|
76
|
+
*
|
|
77
|
+
* Two things a caller does with `quota` are broken by the growing version and neither is exotic:
|
|
78
|
+
*
|
|
79
|
+
* - `usage / quota` as a gauge never fills, because the denominator runs away from the numerator.
|
|
80
|
+
* Ripple showed "2 MB / 10.74 GB" over a library holding 1.78 GB for exactly this reason.
|
|
81
|
+
* - `quota - usage < floor` never fires, so nothing can ever detect pressure. That is what left four
|
|
82
|
+
* of ripple's eviction tests failing for months against a condition that could not occur.
|
|
83
|
+
*
|
|
84
|
+
* So the behaviour picked here is FIREFOX'S: a ceiling, and a ceiling does not rise because you put
|
|
85
|
+
* something under it. It is implemented as the lowest quota seen for this origin, which needs no
|
|
86
|
+
* probe and no user agent sniffing, and it is TRUE in the strong sense: the platform really did say
|
|
87
|
+
* the origin could hold this much, and nothing here invents a number the platform never stated.
|
|
88
|
+
*
|
|
89
|
+
* The cost is deliberate and is in the safe direction. On Chromium, once some bytes are written, the
|
|
90
|
+
* reported headroom is smaller than what could really be written, so a caller reclaims cache
|
|
91
|
+
* slightly early. Cache is the thing that can be fetched again; a caller that never reclaims because
|
|
92
|
+
* it never sees pressure is the failure this replaces.
|
|
101
93
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* reach for the global, which is what keeps the difference in one place instead of at every call
|
|
105
|
-
* site that happens to remember.
|
|
94
|
+
* In memory, and per page. Persisting it would mean owning storage to describe storage, and a reload
|
|
95
|
+
* re-anchors on a figure the platform stated at that moment, which is the same guarantee.
|
|
106
96
|
*/
|
|
97
|
+
var narrowest;
|
|
98
|
+
var ceiling = (reported, usage) => {
|
|
99
|
+
if (reported === void 0) return void 0;
|
|
100
|
+
narrowest = narrowest === void 0 ? reported : Math.min(narrowest, reported);
|
|
101
|
+
return Math.max(narrowest, usage ?? 0);
|
|
102
|
+
};
|
|
107
103
|
var storage = {
|
|
108
104
|
/**
|
|
109
105
|
* Same name and same shape as the platform's, with `usage` MEASURED rather than reported.
|
|
110
106
|
*
|
|
111
|
-
* Falls back to the
|
|
112
|
-
* system, a directory that will not enumerate, or
|
|
113
|
-
*
|
|
114
|
-
* is never observed to be too HIGH.
|
|
107
|
+
* Falls back to the platform's own figure whenever the walk cannot be done: no origin private file
|
|
108
|
+
* system, a directory that will not enumerate, or no `estimate` at all. That is not a silent
|
|
109
|
+
* downgrade, because the platform's figure is a floor rather than a guess.
|
|
115
110
|
*/
|
|
116
111
|
estimate: async () => {
|
|
117
112
|
const native = globalThis.navigator?.storage;
|
|
118
|
-
if (!native?.estimate) return {
|
|
119
|
-
usage: 0,
|
|
120
|
-
quota: 0
|
|
121
|
-
};
|
|
113
|
+
if (!native?.estimate) return {};
|
|
122
114
|
const estimate = await native.estimate();
|
|
123
|
-
const
|
|
115
|
+
const usage = reconcile(estimate, native.getDirectory ? await native.getDirectory().then((directory) => walkBytes(directory)).catch(() => null) : null);
|
|
124
116
|
return {
|
|
125
117
|
...estimate,
|
|
126
|
-
usage
|
|
118
|
+
usage,
|
|
119
|
+
quota: ceiling(estimate.quota, usage)
|
|
127
120
|
};
|
|
128
121
|
},
|
|
129
122
|
persist: () => globalThis.navigator?.storage?.persist?.() ?? Promise.resolve(false),
|
|
@@ -134,54 +127,5 @@ var storage = {
|
|
|
134
127
|
return native.getDirectory();
|
|
135
128
|
}
|
|
136
129
|
};
|
|
137
|
-
/**
|
|
138
|
-
* Which ceiling this origin has, by WRITING and watching, because nothing else can tell.
|
|
139
|
-
*
|
|
140
|
-
* The two shapes are indistinguishable at rest: at low usage a flat quota and a flat headroom are
|
|
141
|
-
* the same pair of numbers, which is exactly how the difference went unnoticed. Only writing
|
|
142
|
-
* separates them, so this writes, and it is therefore not something to call casually.
|
|
143
|
-
*
|
|
144
|
-
* Sparse, so it costs no real disk: the quota system charges a file's EXTENT, and a single byte
|
|
145
|
-
* written `probeBytes - 1` into a file is charged the whole extent instantly. The probe file is
|
|
146
|
-
* removed again, in a `finally`, whether or not the measurement succeeded.
|
|
147
|
-
*
|
|
148
|
-
* Answers `unknown` rather than guessing when the probe cannot be written or the origin refuses it,
|
|
149
|
-
* because "I could not tell" and "the ceiling is fixed" lead to opposite decisions.
|
|
150
|
-
*/
|
|
151
|
-
var measureQuotaCeiling = async ({ probeBytes = 536870912, name = ".ponyfill-quota-probe" } = {}) => {
|
|
152
|
-
const native = globalThis.navigator?.storage;
|
|
153
|
-
if (!native?.estimate || !native.getDirectory) return "unknown";
|
|
154
|
-
let root;
|
|
155
|
-
try {
|
|
156
|
-
root = await native.getDirectory();
|
|
157
|
-
} catch {
|
|
158
|
-
return "unknown";
|
|
159
|
-
}
|
|
160
|
-
const before = await native.estimate();
|
|
161
|
-
try {
|
|
162
|
-
const writable = await (await root.getFileHandle(name, { create: true })).createWritable();
|
|
163
|
-
await writable.write({
|
|
164
|
-
type: "write",
|
|
165
|
-
position: probeBytes - 1,
|
|
166
|
-
data: /* @__PURE__ */ new Uint8Array(1)
|
|
167
|
-
});
|
|
168
|
-
await writable.close();
|
|
169
|
-
const after = await native.estimate();
|
|
170
|
-
const written = (after.usage ?? 0) - (before.usage ?? 0);
|
|
171
|
-
if (written < probeBytes / 2) return "unknown";
|
|
172
|
-
return (before.quota ?? 0) - (before.usage ?? 0) - ((after.quota ?? 0) - (after.usage ?? 0)) < written / 4 ? "elastic" : "fixed";
|
|
173
|
-
} catch {
|
|
174
|
-
return "unknown";
|
|
175
|
-
} finally {
|
|
176
|
-
await root.removeEntry(name).catch(() => {});
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
130
|
//#endregion
|
|
180
|
-
exports.MAX_DEPTH = MAX_DEPTH;
|
|
181
|
-
exports.MAX_ENTRIES = MAX_ENTRIES;
|
|
182
|
-
exports.QUOTA_CEILING = QUOTA_CEILING;
|
|
183
|
-
exports.correctedUsage = correctedUsage;
|
|
184
|
-
exports.isUsageUnderReported = isUsageUnderReported;
|
|
185
|
-
exports.measureDirectoryBytes = measureDirectoryBytes;
|
|
186
|
-
exports.measureQuotaCeiling = measureQuotaCeiling;
|
|
187
131
|
exports.storage = storage;
|
package/build/storage.d.ts
CHANGED
|
@@ -1,180 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `navigator.storage`, with the
|
|
2
|
+
* `navigator.storage`, with the same method names and no extra ones.
|
|
3
3
|
*
|
|
4
|
-
* A PONYFILL, so nothing here touches a global
|
|
5
|
-
*
|
|
4
|
+
* A PONYFILL, so nothing here touches a global and nothing here invents API. You import `storage`
|
|
5
|
+
* and call it exactly as you would the platform's:
|
|
6
6
|
*
|
|
7
7
|
* import { storage } from '@banou/ponyfill'
|
|
8
8
|
* const { usage, quota } = await storage.estimate()
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Only `estimate` behaves differently from the platform's, and only by being CORRECT. The rest are
|
|
11
|
+
* passed straight through so this can be a drop-in for `navigator.storage`, which is the whole point:
|
|
12
|
+
* a caller that had to mix `storage.estimate()` with `navigator.storage.getDirectory()` would be
|
|
13
|
+
* back to remembering which half is safe.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
15
|
+
* ## Why `estimate` cannot be believed as the platform gives it
|
|
16
|
+
*
|
|
17
|
+
* ### `usage` can be six orders of magnitude short
|
|
13
18
|
*
|
|
14
19
|
* MEASURED on Chrome 151: an origin holding a VERIFIED 1,783,407,077 bytes of torrent data reported
|
|
15
|
-
* `usage: 1,813,502`, with `usageDetails.fileSystem: 752`. Not a rounding difference, not a stale
|
|
16
|
-
* cache: 752 bytes against 1.78 GB. Anything deciding whether a write will fit from that figure
|
|
17
|
-
* decides that everything fits, and then the write fails with `QuotaExceededError` at whatever
|
|
18
|
-
* moment the real limit is reached.
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* usage: 1813502
|
|
22
|
+
* usageDetails: { fileSystem: 752, indexedDB: 1809581, serviceWorkerRegistrations: 3169 }
|
|
23
|
+
*
|
|
24
|
+
* 752 bytes against 1.78 GB. Not rounding and not lag, and not universal either: another machine
|
|
25
|
+
* reported the same data correctly, so the figure can neither be trusted nor discarded.
|
|
26
|
+
*
|
|
27
|
+
* So `estimate()` WALKS the origin's file system and reports whichever answer is larger. Larger is
|
|
28
|
+
* the safe direction on purpose. Over-reporting makes a caller reclaim cache slightly early, which
|
|
29
|
+
* is what cache is for; under-reporting makes it decide there is room forever, and what happens then
|
|
30
|
+
* is not a full disk, it is a write failing with `QuotaExceededError` at some unrelated moment.
|
|
31
|
+
*
|
|
32
|
+
* Where `usageDetails` is available the correction is surgical: the file system component is the
|
|
33
|
+
* broken one, so it is replaced with the measurement and the components the browser counts correctly
|
|
34
|
+
* are kept. Without it, the larger of the two is the best available answer.
|
|
24
35
|
*
|
|
25
|
-
* ###
|
|
36
|
+
* ### `quota` means a different thing on each engine
|
|
26
37
|
*
|
|
27
38
|
* MEASURED 2026-09-03, one machine with 2.7 TiB free, one origin, three 512 MiB sparse writes per
|
|
28
39
|
* engine, same page and same code:
|
|
29
40
|
*
|
|
30
|
-
* | engine | quota at rest |
|
|
41
|
+
* | engine | quota at rest | after 1.615 GB written | `quota - usage` |
|
|
31
42
|
* | --- | --- | --- | --- |
|
|
32
|
-
* | Chromium 152 | 10,737,491,968 |
|
|
33
|
-
* | Firefox | 10,737,418,240 |
|
|
43
|
+
* | Chromium 152 | 10,737,491,968 | rose by exactly what was written | 10,737,418,240 every time, moved 0 bytes |
|
|
44
|
+
* | Firefox | 10,737,418,240 | unmoved | fell 536,870,912 per write |
|
|
34
45
|
*
|
|
35
46
|
* Both cap at 10 GiB. They cap DIFFERENT QUANTITIES. Chromium's quota is a FLOATING ceiling,
|
|
36
47
|
* `usage + headroom`, so the headroom is a constant and can never shrink however much is written.
|
|
37
48
|
* Firefox's is a FIXED ceiling, so writing consumes it.
|
|
38
49
|
*
|
|
39
|
-
* Neither is a bug
|
|
50
|
+
* Neither is a bug: the Storage Standard calls quota "a conservative estimate" and never says how to
|
|
40
51
|
* compute it. But it means one very common line is dead on one engine and live on the other:
|
|
41
52
|
*
|
|
42
53
|
* if (quota - usage < someFloor) { ... } // can never be true on Chromium
|
|
43
54
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* number to invent: on Chromium you really can write 10 more GiB, so the native answer is true. What
|
|
50
|
-
* it does is name the two shapes, say which one an origin has, and refuse to let the difference be
|
|
51
|
-
* discovered again by somebody debugging a dead branch. See {@link QUOTA_CEILING} and
|
|
52
|
-
* {@link measureQuotaCeiling}.
|
|
55
|
+
* Nothing here papers over that, because there is no honest number to substitute: on Chromium you
|
|
56
|
+
* really can write 10 more GiB, so the platform's answer is true. It is written down here because
|
|
57
|
+
* the two shapes are indistinguishable at rest, which is how the difference went unnoticed long
|
|
58
|
+
* enough to leave four of ripple's storage tests failing for months against a condition that could
|
|
59
|
+
* never occur.
|
|
53
60
|
*/
|
|
54
|
-
/** The same shape `navigator.storage.estimate()` resolves to, plus what the browser volunteered. */
|
|
55
|
-
export type StorageEstimate = {
|
|
56
|
-
usage: number;
|
|
57
|
-
quota: number;
|
|
58
|
-
usageDetails?: Record<string, number>;
|
|
59
|
-
};
|
|
60
61
|
/**
|
|
61
|
-
*
|
|
62
|
+
* The platform's own `StorageEstimate`, named the same.
|
|
62
63
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* constant and never signals pressure. `unknown` is the honest answer before anything has measured
|
|
66
|
-
* it, and it is the default, because the alternative is sniffing the user agent.
|
|
64
|
+
* Every field optional exactly as the specification has them, so this is a drop-in for what
|
|
65
|
+
* `navigator.storage.estimate()` resolves to rather than a stricter thing a caller has to adapt to.
|
|
67
66
|
*/
|
|
68
|
-
export type
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*/
|
|
76
|
-
export declare const QUOTA_CEILING: Record<string, {
|
|
77
|
-
ceiling: QuotaCeiling;
|
|
78
|
-
measured: string;
|
|
79
|
-
note: string;
|
|
80
|
-
}>;
|
|
81
|
-
/** A directory handle, narrowed to the two members a size walk actually touches. */
|
|
82
|
-
type WalkableDirectory = {
|
|
83
|
-
values: () => AsyncIterable<WalkableDirectory | WalkableFile>;
|
|
84
|
-
kind?: string;
|
|
85
|
-
};
|
|
86
|
-
type WalkableFile = {
|
|
87
|
-
kind?: string;
|
|
88
|
-
getFile: () => Promise<{
|
|
89
|
-
size: number;
|
|
90
|
-
}>;
|
|
67
|
+
export type StorageEstimate = {
|
|
68
|
+
usage?: number;
|
|
69
|
+
quota?: number;
|
|
70
|
+
/** Chrome only, and the whole reason the correction can be surgical rather than merely larger. */
|
|
71
|
+
usageDetails?: {
|
|
72
|
+
fileSystem?: number;
|
|
73
|
+
};
|
|
91
74
|
};
|
|
92
|
-
/**
|
|
93
|
-
* Never walk forever: a cycle is impossible in OPFS, but a pathological tree is not worth the tick.
|
|
94
|
-
*
|
|
95
|
-
* These are bounds, not correctness: hitting either returns what was counted so far rather than
|
|
96
|
-
* failing, which keeps the answer a floor in the same direction as everything else here.
|
|
97
|
-
*/
|
|
98
|
-
export declare const MAX_DEPTH = 8;
|
|
99
|
-
export declare const MAX_ENTRIES = 20000;
|
|
100
|
-
/**
|
|
101
|
-
* Every byte under a directory, measured rather than asked for, or `null` when the walk could not
|
|
102
|
-
* be done at all.
|
|
103
|
-
*
|
|
104
|
-
* The null is a distinct third answer and callers depend on it: "the origin holds nothing" and "the
|
|
105
|
-
* file system was unreachable" lead to opposite decisions, and collapsing them to 0 would have
|
|
106
|
-
* {@link correctedUsage} report an empty origin for one that simply could not be read.
|
|
107
|
-
*
|
|
108
|
-
* Counts a file's REPORTED SIZE, which for OPFS is its extent rather than how much of it has been
|
|
109
|
-
* written. That is the same accounting the quota system uses, which is the point: a one byte write a
|
|
110
|
-
* gigabyte into a file is charged a gigabyte, and a measurement disagreeing with the charge would be
|
|
111
|
-
* no more useful than the figure it replaces.
|
|
112
|
-
*
|
|
113
|
-
* An unreadable ENTRY is skipped rather than fatal. A file the engine currently holds an exclusive
|
|
114
|
-
* sync access handle for cannot be opened by anyone else, and `getFile()` on it throws rather than
|
|
115
|
-
* waiting, so a walk that gave up on the first of those would return nothing for exactly the origins
|
|
116
|
-
* holding the most. The answer is therefore a floor: short by the files open right now, never long.
|
|
117
|
-
*/
|
|
118
|
-
export declare const measureDirectoryBytes: (directory: WalkableDirectory, { maxDepth, maxEntries }?: {
|
|
119
|
-
maxDepth?: number;
|
|
120
|
-
maxEntries?: number;
|
|
121
|
-
}) => Promise<number | null>;
|
|
122
|
-
/**
|
|
123
|
-
* The usage figure to believe, given what the browser said and what a walk found.
|
|
124
|
-
*
|
|
125
|
-
* The larger of the two, and the reason is asymmetric: a browser that OVER-reports has never been
|
|
126
|
-
* observed, while one that under-reports by six orders of magnitude has. So the walk can only ever
|
|
127
|
-
* raise the answer.
|
|
128
|
-
*
|
|
129
|
-
* `usageDetails.fileSystem` is subtracted out before the walk is added when the browser volunteers
|
|
130
|
-
* it, because everything else the browser counts (IndexedDB, caches, service worker registrations)
|
|
131
|
-
* it counts correctly and the walk cannot see any of it. Without that split, an origin holding real
|
|
132
|
-
* IndexedDB data would have it silently dropped from the total.
|
|
133
|
-
*
|
|
134
|
-
* Pure, so the arithmetic is testable without a browser: this is the part that can be wrong in a way
|
|
135
|
-
* no integration test would notice.
|
|
136
|
-
*/
|
|
137
|
-
export declare const correctedUsage: (estimate: Partial<StorageEstimate>, walkedBytes: number | null) => number | null;
|
|
138
|
-
/** True when the browser's own figure is too far below the measured one to be believed. */
|
|
139
|
-
export declare const isUsageUnderReported: (estimate: Partial<StorageEstimate>, walkedBytes: number | null) => boolean;
|
|
140
|
-
/**
|
|
141
|
-
* `navigator.storage`, with the same method names.
|
|
142
|
-
*
|
|
143
|
-
* Only `estimate` behaves differently from the platform's, and only in the way documented at the top
|
|
144
|
-
* of this file. The rest are passed straight through so that a caller can import this once and never
|
|
145
|
-
* reach for the global, which is what keeps the difference in one place instead of at every call
|
|
146
|
-
* site that happens to remember.
|
|
147
|
-
*/
|
|
148
75
|
export declare const storage: {
|
|
149
76
|
/**
|
|
150
77
|
* Same name and same shape as the platform's, with `usage` MEASURED rather than reported.
|
|
151
78
|
*
|
|
152
|
-
* Falls back to the
|
|
153
|
-
* system, a directory that will not enumerate, or
|
|
154
|
-
*
|
|
155
|
-
* is never observed to be too HIGH.
|
|
79
|
+
* Falls back to the platform's own figure whenever the walk cannot be done: no origin private file
|
|
80
|
+
* system, a directory that will not enumerate, or no `estimate` at all. That is not a silent
|
|
81
|
+
* downgrade, because the platform's figure is a floor rather than a guess.
|
|
156
82
|
*/
|
|
157
83
|
estimate: () => Promise<StorageEstimate>;
|
|
158
84
|
persist: () => Promise<boolean>;
|
|
159
85
|
persisted: () => Promise<boolean>;
|
|
160
86
|
getDirectory: () => Promise<FileSystemDirectoryHandle>;
|
|
161
87
|
};
|
|
162
|
-
/**
|
|
163
|
-
* Which ceiling this origin has, by WRITING and watching, because nothing else can tell.
|
|
164
|
-
*
|
|
165
|
-
* The two shapes are indistinguishable at rest: at low usage a flat quota and a flat headroom are
|
|
166
|
-
* the same pair of numbers, which is exactly how the difference went unnoticed. Only writing
|
|
167
|
-
* separates them, so this writes, and it is therefore not something to call casually.
|
|
168
|
-
*
|
|
169
|
-
* Sparse, so it costs no real disk: the quota system charges a file's EXTENT, and a single byte
|
|
170
|
-
* written `probeBytes - 1` into a file is charged the whole extent instantly. The probe file is
|
|
171
|
-
* removed again, in a `finally`, whether or not the measurement succeeded.
|
|
172
|
-
*
|
|
173
|
-
* Answers `unknown` rather than guessing when the probe cannot be written or the origin refuses it,
|
|
174
|
-
* because "I could not tell" and "the ceiling is fixed" lead to opposite decisions.
|
|
175
|
-
*/
|
|
176
|
-
export declare const measureQuotaCeiling: ({ probeBytes, name }?: {
|
|
177
|
-
probeBytes?: number;
|
|
178
|
-
name?: string;
|
|
179
|
-
}) => Promise<QuotaCeiling>;
|
|
180
|
-
export {};
|
package/build/storage.js
CHANGED
|
@@ -1,57 +1,38 @@
|
|
|
1
1
|
//#region src/storage.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Never walk forever: a cycle is impossible in the origin private file system, but a pathological
|
|
4
|
+
* tree is not, and neither is a directory with a hundred thousand entries in it.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* {@link measureQuotaCeiling} is what answers the question for the origin actually in front of you.
|
|
8
|
-
*/
|
|
9
|
-
var QUOTA_CEILING = {
|
|
10
|
-
chromium: {
|
|
11
|
-
ceiling: "elastic",
|
|
12
|
-
measured: "2026-09-03, Chrome 152.0.7977.64, 2.7 TiB free",
|
|
13
|
-
note: "quota rose 10.737 GB to 12.353 GB across 1.615 GB written, leaving quota - usage at 10,737,418,240 bytes after every write, unmoved to the byte"
|
|
14
|
-
},
|
|
15
|
-
firefox: {
|
|
16
|
-
ceiling: "fixed",
|
|
17
|
-
measured: "2026-09-03, Playwright firefox 1532, same machine and origin",
|
|
18
|
-
note: "quota held at 10,737,418,240 while the headroom fell by the 1,613,063,025 bytes written, byte for byte"
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
var isDirectory = (handle) => typeof handle.values === "function";
|
|
22
|
-
/**
|
|
23
|
-
* Never walk forever: a cycle is impossible in OPFS, but a pathological tree is not worth the tick.
|
|
24
|
-
*
|
|
25
|
-
* These are bounds, not correctness: hitting either returns what was counted so far rather than
|
|
26
|
-
* failing, which keeps the answer a floor in the same direction as everything else here.
|
|
6
|
+
* Bounds rather than correctness. Hitting either returns what was counted so far instead of failing,
|
|
7
|
+
* which keeps the answer a floor in the same direction as everything else here.
|
|
27
8
|
*/
|
|
28
9
|
var MAX_DEPTH = 8;
|
|
29
10
|
var MAX_ENTRIES = 2e4;
|
|
11
|
+
var isDirectory = (handle) => typeof handle.values === "function";
|
|
30
12
|
/**
|
|
31
|
-
* Every byte under a directory,
|
|
32
|
-
* be done at all.
|
|
13
|
+
* Every byte under a directory, or `null` when the walk could not be done at all.
|
|
33
14
|
*
|
|
34
|
-
* The null is a distinct third answer and
|
|
35
|
-
* file system was unreachable" lead to opposite decisions, and collapsing them to
|
|
36
|
-
*
|
|
15
|
+
* The null is a distinct third answer and the correction below depends on it: "the origin holds
|
|
16
|
+
* nothing" and "the file system was unreachable" lead to opposite decisions, and collapsing them to
|
|
17
|
+
* 0 would report an empty origin for one that simply could not be read.
|
|
37
18
|
*
|
|
38
|
-
* Counts a file's REPORTED SIZE, which for
|
|
39
|
-
* written. That is the same accounting the quota system uses, which is the point: a one byte
|
|
40
|
-
* gigabyte into a file is charged a gigabyte, and a measurement
|
|
41
|
-
* no more useful than the figure it replaces.
|
|
19
|
+
* Counts a file's REPORTED SIZE, which for this file system is its extent rather than how much of it
|
|
20
|
+
* has been written. That is the same accounting the quota system uses, which is the point: a one byte
|
|
21
|
+
* write a gigabyte into a file is charged a gigabyte, and a measurement that disagreed with the
|
|
22
|
+
* charge would be no more useful than the figure it replaces.
|
|
42
23
|
*
|
|
43
|
-
* An unreadable ENTRY is skipped rather than fatal. A file
|
|
44
|
-
* sync access handle for cannot be opened
|
|
24
|
+
* An unreadable ENTRY is skipped rather than fatal. A file something else currently holds an
|
|
25
|
+
* exclusive sync access handle for cannot be opened, and `getFile()` on it throws rather than
|
|
45
26
|
* waiting, so a walk that gave up on the first of those would return nothing for exactly the origins
|
|
46
27
|
* holding the most. The answer is therefore a floor: short by the files open right now, never long.
|
|
47
28
|
*/
|
|
48
|
-
var
|
|
29
|
+
var walkBytes = async (directory) => {
|
|
49
30
|
let total = 0;
|
|
50
31
|
let seen = 0;
|
|
51
32
|
const visit = async (handle, depth) => {
|
|
52
|
-
if (depth >
|
|
33
|
+
if (depth > MAX_DEPTH) return;
|
|
53
34
|
for await (const child of handle.values()) {
|
|
54
|
-
if (++seen >
|
|
35
|
+
if (++seen > MAX_ENTRIES) return;
|
|
55
36
|
if (isDirectory(child)) {
|
|
56
37
|
await visit(child, depth + 1);
|
|
57
38
|
continue;
|
|
@@ -68,61 +49,73 @@ var measureDirectoryBytes = async (directory, { maxDepth = 8, maxEntries = MAX_E
|
|
|
68
49
|
}
|
|
69
50
|
};
|
|
70
51
|
/**
|
|
71
|
-
* The usage
|
|
52
|
+
* The usage to report, given what the platform said and what the walk found.
|
|
72
53
|
*
|
|
73
|
-
* The
|
|
74
|
-
*
|
|
75
|
-
* raise the answer.
|
|
76
|
-
*
|
|
77
|
-
* `usageDetails.fileSystem` is subtracted out before the walk is added when the browser volunteers
|
|
78
|
-
* it, because everything else the browser counts (IndexedDB, caches, service worker registrations)
|
|
79
|
-
* it counts correctly and the walk cannot see any of it. Without that split, an origin holding real
|
|
80
|
-
* IndexedDB data would have it silently dropped from the total.
|
|
81
|
-
*
|
|
82
|
-
* Pure, so the arithmetic is testable without a browser: this is the part that can be wrong in a way
|
|
83
|
-
* no integration test would notice.
|
|
54
|
+
* The walk can only ever RAISE the answer: a browser over-reporting has never been observed, while
|
|
55
|
+
* one under-reporting by six orders of magnitude has.
|
|
84
56
|
*/
|
|
85
|
-
var
|
|
57
|
+
var reconcile = (estimate, walked) => {
|
|
86
58
|
const reported = estimate.usage;
|
|
87
|
-
if (
|
|
88
|
-
if (reported === void 0) return
|
|
59
|
+
if (walked === null || !Number.isFinite(walked) || walked < 0) return reported;
|
|
60
|
+
if (reported === void 0) return walked;
|
|
89
61
|
const fileSystem = estimate.usageDetails?.fileSystem;
|
|
90
62
|
if (typeof fileSystem === "number") {
|
|
91
63
|
const other = Math.max(0, reported - fileSystem);
|
|
92
|
-
return Math.max(reported,
|
|
64
|
+
return Math.max(reported, walked + other);
|
|
93
65
|
}
|
|
94
|
-
return Math.max(reported,
|
|
66
|
+
return Math.max(reported, walked);
|
|
95
67
|
};
|
|
96
|
-
/** True when the browser's own figure is too far below the measured one to be believed. */
|
|
97
|
-
var isUsageUnderReported = (estimate, walkedBytes) => walkedBytes !== null && walkedBytes > 0 && (estimate.usage ?? 0) < walkedBytes / 2;
|
|
98
68
|
/**
|
|
99
|
-
*
|
|
69
|
+
* The narrowest quota this origin has reported, which is the one that gets reported back.
|
|
70
|
+
*
|
|
71
|
+
* THE DIVERGENCE, AND THE PICK. `quota - usage` already means the same on both engines: bytes you
|
|
72
|
+
* can still write. What diverges is `quota` as a TOTAL, and Chromium never states one. It states
|
|
73
|
+
* `usage + headroom`, recomputed on every read, so the number grows as you fill the origin: measured
|
|
74
|
+
* 10.737 GB rising to 12.353 GB across 1.615 GB written. Firefox states a real total and holds it.
|
|
75
|
+
*
|
|
76
|
+
* Two things a caller does with `quota` are broken by the growing version and neither is exotic:
|
|
77
|
+
*
|
|
78
|
+
* - `usage / quota` as a gauge never fills, because the denominator runs away from the numerator.
|
|
79
|
+
* Ripple showed "2 MB / 10.74 GB" over a library holding 1.78 GB for exactly this reason.
|
|
80
|
+
* - `quota - usage < floor` never fires, so nothing can ever detect pressure. That is what left four
|
|
81
|
+
* of ripple's eviction tests failing for months against a condition that could not occur.
|
|
82
|
+
*
|
|
83
|
+
* So the behaviour picked here is FIREFOX'S: a ceiling, and a ceiling does not rise because you put
|
|
84
|
+
* something under it. It is implemented as the lowest quota seen for this origin, which needs no
|
|
85
|
+
* probe and no user agent sniffing, and it is TRUE in the strong sense: the platform really did say
|
|
86
|
+
* the origin could hold this much, and nothing here invents a number the platform never stated.
|
|
87
|
+
*
|
|
88
|
+
* The cost is deliberate and is in the safe direction. On Chromium, once some bytes are written, the
|
|
89
|
+
* reported headroom is smaller than what could really be written, so a caller reclaims cache
|
|
90
|
+
* slightly early. Cache is the thing that can be fetched again; a caller that never reclaims because
|
|
91
|
+
* it never sees pressure is the failure this replaces.
|
|
100
92
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* reach for the global, which is what keeps the difference in one place instead of at every call
|
|
104
|
-
* site that happens to remember.
|
|
93
|
+
* In memory, and per page. Persisting it would mean owning storage to describe storage, and a reload
|
|
94
|
+
* re-anchors on a figure the platform stated at that moment, which is the same guarantee.
|
|
105
95
|
*/
|
|
96
|
+
var narrowest;
|
|
97
|
+
var ceiling = (reported, usage) => {
|
|
98
|
+
if (reported === void 0) return void 0;
|
|
99
|
+
narrowest = narrowest === void 0 ? reported : Math.min(narrowest, reported);
|
|
100
|
+
return Math.max(narrowest, usage ?? 0);
|
|
101
|
+
};
|
|
106
102
|
var storage = {
|
|
107
103
|
/**
|
|
108
104
|
* Same name and same shape as the platform's, with `usage` MEASURED rather than reported.
|
|
109
105
|
*
|
|
110
|
-
* Falls back to the
|
|
111
|
-
* system, a directory that will not enumerate, or
|
|
112
|
-
*
|
|
113
|
-
* is never observed to be too HIGH.
|
|
106
|
+
* Falls back to the platform's own figure whenever the walk cannot be done: no origin private file
|
|
107
|
+
* system, a directory that will not enumerate, or no `estimate` at all. That is not a silent
|
|
108
|
+
* downgrade, because the platform's figure is a floor rather than a guess.
|
|
114
109
|
*/
|
|
115
110
|
estimate: async () => {
|
|
116
111
|
const native = globalThis.navigator?.storage;
|
|
117
|
-
if (!native?.estimate) return {
|
|
118
|
-
usage: 0,
|
|
119
|
-
quota: 0
|
|
120
|
-
};
|
|
112
|
+
if (!native?.estimate) return {};
|
|
121
113
|
const estimate = await native.estimate();
|
|
122
|
-
const
|
|
114
|
+
const usage = reconcile(estimate, native.getDirectory ? await native.getDirectory().then((directory) => walkBytes(directory)).catch(() => null) : null);
|
|
123
115
|
return {
|
|
124
116
|
...estimate,
|
|
125
|
-
usage
|
|
117
|
+
usage,
|
|
118
|
+
quota: ceiling(estimate.quota, usage)
|
|
126
119
|
};
|
|
127
120
|
},
|
|
128
121
|
persist: () => globalThis.navigator?.storage?.persist?.() ?? Promise.resolve(false),
|
|
@@ -133,47 +126,5 @@ var storage = {
|
|
|
133
126
|
return native.getDirectory();
|
|
134
127
|
}
|
|
135
128
|
};
|
|
136
|
-
/**
|
|
137
|
-
* Which ceiling this origin has, by WRITING and watching, because nothing else can tell.
|
|
138
|
-
*
|
|
139
|
-
* The two shapes are indistinguishable at rest: at low usage a flat quota and a flat headroom are
|
|
140
|
-
* the same pair of numbers, which is exactly how the difference went unnoticed. Only writing
|
|
141
|
-
* separates them, so this writes, and it is therefore not something to call casually.
|
|
142
|
-
*
|
|
143
|
-
* Sparse, so it costs no real disk: the quota system charges a file's EXTENT, and a single byte
|
|
144
|
-
* written `probeBytes - 1` into a file is charged the whole extent instantly. The probe file is
|
|
145
|
-
* removed again, in a `finally`, whether or not the measurement succeeded.
|
|
146
|
-
*
|
|
147
|
-
* Answers `unknown` rather than guessing when the probe cannot be written or the origin refuses it,
|
|
148
|
-
* because "I could not tell" and "the ceiling is fixed" lead to opposite decisions.
|
|
149
|
-
*/
|
|
150
|
-
var measureQuotaCeiling = async ({ probeBytes = 536870912, name = ".ponyfill-quota-probe" } = {}) => {
|
|
151
|
-
const native = globalThis.navigator?.storage;
|
|
152
|
-
if (!native?.estimate || !native.getDirectory) return "unknown";
|
|
153
|
-
let root;
|
|
154
|
-
try {
|
|
155
|
-
root = await native.getDirectory();
|
|
156
|
-
} catch {
|
|
157
|
-
return "unknown";
|
|
158
|
-
}
|
|
159
|
-
const before = await native.estimate();
|
|
160
|
-
try {
|
|
161
|
-
const writable = await (await root.getFileHandle(name, { create: true })).createWritable();
|
|
162
|
-
await writable.write({
|
|
163
|
-
type: "write",
|
|
164
|
-
position: probeBytes - 1,
|
|
165
|
-
data: /* @__PURE__ */ new Uint8Array(1)
|
|
166
|
-
});
|
|
167
|
-
await writable.close();
|
|
168
|
-
const after = await native.estimate();
|
|
169
|
-
const written = (after.usage ?? 0) - (before.usage ?? 0);
|
|
170
|
-
if (written < probeBytes / 2) return "unknown";
|
|
171
|
-
return (before.quota ?? 0) - (before.usage ?? 0) - ((after.quota ?? 0) - (after.usage ?? 0)) < written / 4 ? "elastic" : "fixed";
|
|
172
|
-
} catch {
|
|
173
|
-
return "unknown";
|
|
174
|
-
} finally {
|
|
175
|
-
await root.removeEntry(name).catch(() => {});
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
129
|
//#endregion
|
|
179
|
-
export {
|
|
130
|
+
export { storage };
|