@file-viewer/ppt 0.3.1 → 0.3.3
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/NOTICE +0 -3
- package/README.md +4 -4
- package/index.mjs +1 -1
- package/manifest.json +4 -4
- package/package.json +1 -1
- package/ppt-native.wasm +0 -0
- package/worker.mjs +731 -1
package/NOTICE
CHANGED
|
@@ -62,7 +62,6 @@ requirement for fonts to remain under this license does not apply to
|
|
|
62
62
|
any document created using the fonts or their derivatives.
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
|
|
66
65
|
DEFINITIONS
|
|
67
66
|
"Font Software" refers to the set of files released by the Copyright
|
|
68
67
|
Holder(s) under this license and clearly marked as such.
|
|
@@ -118,12 +117,10 @@ Software, subject to the following conditions:
|
|
|
118
117
|
Software.
|
|
119
118
|
|
|
120
119
|
|
|
121
|
-
|
|
122
120
|
TERMINATION
|
|
123
121
|
This license becomes null and void if any of the above conditions are not met.
|
|
124
122
|
|
|
125
123
|
|
|
126
|
-
|
|
127
124
|
DISCLAIMER
|
|
128
125
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
129
126
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
package/README.md
CHANGED
|
@@ -193,9 +193,9 @@ moves assets, configure them explicitly:
|
|
|
193
193
|
|
|
194
194
|
```js
|
|
195
195
|
const viewer = await createPptViewer({
|
|
196
|
-
workerUrl: new URL('/vendor/file-viewer/ppt/0.3.
|
|
197
|
-
wasmUrl: new URL('/vendor/file-viewer/ppt/0.3.
|
|
198
|
-
fontUrl: new URL('/vendor/file-viewer/ppt/0.3.
|
|
196
|
+
workerUrl: new URL('/vendor/file-viewer/ppt/0.3.3/worker.mjs', location.origin),
|
|
197
|
+
wasmUrl: new URL('/vendor/file-viewer/ppt/0.3.3/ppt-native.wasm', location.origin),
|
|
198
|
+
fontUrl: new URL('/vendor/file-viewer/ppt/0.3.3/ppt-font-cjk.otf', location.origin)
|
|
199
199
|
});
|
|
200
200
|
```
|
|
201
201
|
|
|
@@ -447,7 +447,7 @@ manifest.json
|
|
|
447
447
|
`application/wasm`,`.otf` 使用 `font/otf`,`.ppt` 使用
|
|
448
448
|
`application/vnd.ms-powerpoint`。
|
|
449
449
|
|
|
450
|
-
将资源发布在版本化或内容寻址目录(如 `/vendor/file-viewer/ppt/0.3.
|
|
450
|
+
将资源发布在版本化或内容寻址目录(如 `/vendor/file-viewer/ppt/0.3.3/`)后,
|
|
451
451
|
可配置:
|
|
452
452
|
|
|
453
453
|
```http
|
package/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const MANIFEST = Object.freeze({"kind":"flyfish-ppt-public-native-wasm-v2","product":"Flyfish PPT Viewer","format":"PowerPoint 97-2003 (.ppt)","packageName":"@file-viewer/ppt","packageVersion":"0.3.
|
|
1
|
+
const MANIFEST = Object.freeze({"kind":"flyfish-ppt-public-native-wasm-v2","product":"Flyfish PPT Viewer","format":"PowerPoint 97-2003 (.ppt)","packageName":"@file-viewer/ppt","packageVersion":"0.3.3","engine":"flyfish-classic-ppt-native-engine","engineBuild":"d2b522ef23b5c08a","id":"FV-PPT-PUBLIC-WATERMARKED-V2","feature":"ppt","edition":"public-watermarked","holder":"Individuals and Organizations","distribution":"proprietary-public-binary","commercialUse":true,"publicAnyOrigin":true,"watermarkRequired":true,"watermarkText":"Flyfish Viewer","usePolicy":"public-watermarked-general-use","licensePolicy":"flyfish-public-watermarked-binary-license-v2","sourceCodeRights":false,"sourceDeliveryRights":false,"apacheLicenseApplies":false,"redistributionAllowed":true,"redistributionPolicy":"unmodified-integrated-bundling-only","standaloneRedistributionAllowed":false,"watermarkRemovalRequiresCommercialAuthorization":true,"runtimeLicenseRequired":false,"renderer":"native-wasm-worker-offscreen-canvas","integrity":"native-wasm-font-sha256-v2","wasmFile":"ppt-native.wasm","wasmBytes":1557729,"workerFile":"worker.mjs","fontPack":{"kind":"flyfish-cjk-font-pack-v1","file":"ppt-font-cjk.otf","bytes":16437364,"sha256":"2c76254f6fc379fddfce0a7e84fb5385bb135d3e399294f6eeb6680d0365b74b"},"workerDefault":true,"virtualScrollDefault":true,"frameCache":"indexeddb-final-watermarked-png-lru","watermarkEnforcement":"native-final-frame","wasmSha256":"5e1313c50fee36c9573527bc3bf88920ea54b25016ee72d369bb7b27df1540ba"});
|
|
2
2
|
const DEFAULT_WASM_URL = new URL('./ppt-native.wasm', import.meta.url);
|
|
3
3
|
const DEFAULT_FONT_URL = new URL(`./${MANIFEST.fontPack.file}`, import.meta.url);
|
|
4
4
|
const DEFAULT_WORKER_URL = new URL(`./${MANIFEST.workerFile}`, import.meta.url);
|
package/manifest.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"product": "Flyfish PPT Viewer",
|
|
4
4
|
"format": "PowerPoint 97-2003 (.ppt)",
|
|
5
5
|
"packageName": "@file-viewer/ppt",
|
|
6
|
-
"packageVersion": "0.3.
|
|
6
|
+
"packageVersion": "0.3.3",
|
|
7
7
|
"engine": "flyfish-classic-ppt-native-engine",
|
|
8
|
-
"engineBuild": "
|
|
8
|
+
"engineBuild": "d2b522ef23b5c08a",
|
|
9
9
|
"id": "FV-PPT-PUBLIC-WATERMARKED-V2",
|
|
10
10
|
"feature": "ppt",
|
|
11
11
|
"edition": "public-watermarked",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"renderer": "native-wasm-worker-offscreen-canvas",
|
|
29
29
|
"integrity": "native-wasm-font-sha256-v2",
|
|
30
30
|
"wasmFile": "ppt-native.wasm",
|
|
31
|
-
"wasmBytes":
|
|
31
|
+
"wasmBytes": 1557729,
|
|
32
32
|
"workerFile": "worker.mjs",
|
|
33
33
|
"fontPack": {
|
|
34
34
|
"kind": "flyfish-cjk-font-pack-v1",
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"virtualScrollDefault": true,
|
|
41
41
|
"frameCache": "indexeddb-final-watermarked-png-lru",
|
|
42
42
|
"watermarkEnforcement": "native-final-frame",
|
|
43
|
-
"wasmSha256": "
|
|
43
|
+
"wasmSha256": "5e1313c50fee36c9573527bc3bf88920ea54b25016ee72d369bb7b27df1540ba"
|
|
44
44
|
}
|
package/package.json
CHANGED
package/ppt-native.wasm
CHANGED
|
Binary file
|
package/worker.mjs
CHANGED
|
@@ -1,4 +1,734 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, best-effort cache for final watermarked PNG frames.
|
|
3
|
+
*
|
|
4
|
+
* This module is intentionally storage-only. It does not parse PPT data, inspect
|
|
5
|
+
* font assets, or transform renderer output. Every storage failure degrades to a
|
|
6
|
+
* cache miss so IndexedDB can never become a rendering dependency.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DB_VERSION = 1;
|
|
10
|
+
const FRAME_STORE = "frames";
|
|
11
|
+
const META_STORE = "meta";
|
|
12
|
+
const DOCUMENT_INDEX = "byDocument";
|
|
13
|
+
const ACCESS_INDEX = "byLastAccess";
|
|
14
|
+
const USAGE_KEY = "usage";
|
|
15
|
+
|
|
16
|
+
const MIB = 1024 * 1024;
|
|
17
|
+
const DEFAULT_DB_NAME = "flyfish-ppt-frame-cache-v1";
|
|
18
|
+
const DEFAULT_MAX_BYTES = 256 * MIB;
|
|
19
|
+
const DEFAULT_MAX_ENTRIES = 256;
|
|
20
|
+
const DEFAULT_MAX_ITEM_BYTES = 32 * MIB;
|
|
21
|
+
const MAX_DATABASE_NAME_LENGTH = 128;
|
|
22
|
+
const MAX_DOCUMENT_ID_LENGTH = 256;
|
|
23
|
+
const MAX_VARIANT_LENGTH = 256;
|
|
24
|
+
const MAX_PAGE_INDEX = 10_000_000;
|
|
25
|
+
const MAX_DIMENSION = 65_535;
|
|
26
|
+
const OPEN_TIMEOUT_MS = 1_000;
|
|
27
|
+
|
|
28
|
+
function clampInteger(value, fallback, minimum, maximum) {
|
|
29
|
+
let numeric;
|
|
30
|
+
try {
|
|
31
|
+
numeric = Number(value);
|
|
32
|
+
} catch {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
if (!Number.isFinite(numeric)) {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
return Math.min(maximum, Math.max(minimum, Math.trunc(numeric)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeBoundedString(value, maximumLength, { trim = false } = {}) {
|
|
42
|
+
if (typeof value !== "string") {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const normalized = trim ? value.trim() : value;
|
|
46
|
+
if (normalized.length === 0 || normalized.length > maximumLength) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pure limit normalization used by both production code and unit tests.
|
|
54
|
+
*/
|
|
55
|
+
function normalizeFrameCacheLimits(options = {}) {
|
|
56
|
+
const source = options && typeof options === "object" ? options : {};
|
|
57
|
+
const maxBytes = clampInteger(source.maxBytes, DEFAULT_MAX_BYTES, 0, 2 * 1024 * MIB);
|
|
58
|
+
const maxEntries = clampInteger(source.maxEntries, DEFAULT_MAX_ENTRIES, 0, 4_096);
|
|
59
|
+
const defaultItemBytes = Math.min(DEFAULT_MAX_ITEM_BYTES, maxBytes);
|
|
60
|
+
const configuredItemBytes = source.maxItemBytes ?? source.maxEntryBytes;
|
|
61
|
+
const maxItemBytes = maxBytes === 0
|
|
62
|
+
? 0
|
|
63
|
+
: clampInteger(configuredItemBytes, defaultItemBytes, 0, maxBytes);
|
|
64
|
+
return Object.freeze({ maxBytes, maxEntries, maxItemBytes });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Pure cache-key normalization. Invalid or potentially collision-prone inputs
|
|
69
|
+
* return null instead of being truncated.
|
|
70
|
+
*/
|
|
71
|
+
function normalizeFrameCacheKey(input, pageIndex, variant = "") {
|
|
72
|
+
const source = input && typeof input === "object"
|
|
73
|
+
? input
|
|
74
|
+
: { documentId: input, pageIndex, variant };
|
|
75
|
+
|
|
76
|
+
const documentId = normalizeBoundedString(source.documentId, MAX_DOCUMENT_ID_LENGTH, { trim: true });
|
|
77
|
+
let normalizedPageIndex;
|
|
78
|
+
let normalizedVariant;
|
|
79
|
+
try {
|
|
80
|
+
normalizedPageIndex = Number(source.pageIndex);
|
|
81
|
+
normalizedVariant = source.variant == null ? "" : String(source.variant);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
documentId == null
|
|
88
|
+
|| !Number.isSafeInteger(normalizedPageIndex)
|
|
89
|
+
|| normalizedPageIndex < 0
|
|
90
|
+
|| normalizedPageIndex > MAX_PAGE_INDEX
|
|
91
|
+
|| normalizedVariant.length > MAX_VARIANT_LENGTH
|
|
92
|
+
) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Length-prefixing prevents separators inside opaque identifiers from
|
|
97
|
+
// creating collisions.
|
|
98
|
+
const key = `v1:${documentId.length}:${documentId}:${normalizedPageIndex}:${normalizedVariant.length}:${normalizedVariant}`;
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
key,
|
|
101
|
+
documentId,
|
|
102
|
+
pageIndex: normalizedPageIndex,
|
|
103
|
+
variant: normalizedVariant,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createFrameCacheKey(documentId, pageIndex, variant = "") {
|
|
108
|
+
return normalizeFrameCacheKey(documentId, pageIndex, variant)?.key ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeDatabaseName(value) {
|
|
112
|
+
return normalizeBoundedString(value, MAX_DATABASE_NAME_LENGTH, { trim: true }) ?? DEFAULT_DB_NAME;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isPngBlob(blob) {
|
|
116
|
+
return Boolean(
|
|
117
|
+
blob
|
|
118
|
+
&& typeof blob === "object"
|
|
119
|
+
&& Number.isSafeInteger(blob.size)
|
|
120
|
+
&& blob.size > 0
|
|
121
|
+
&& typeof blob.arrayBuffer === "function"
|
|
122
|
+
&& typeof blob.type === "string"
|
|
123
|
+
&& blob.type.toLowerCase() === "image/png",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeDimension(value) {
|
|
128
|
+
let numeric;
|
|
129
|
+
try {
|
|
130
|
+
numeric = Number(value);
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return Number.isSafeInteger(numeric) && numeric > 0 && numeric <= MAX_DIMENSION
|
|
135
|
+
? numeric
|
|
136
|
+
: null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizePutArguments(keyInput, frameInput, limits) {
|
|
140
|
+
const frame = frameInput && typeof frameInput === "object" ? frameInput : keyInput;
|
|
141
|
+
const key = normalizeFrameCacheKey(keyInput);
|
|
142
|
+
const width = normalizeDimension(frame?.width);
|
|
143
|
+
const height = normalizeDimension(frame?.height);
|
|
144
|
+
const blob = frame?.blob;
|
|
145
|
+
|
|
146
|
+
if (key == null || width == null || height == null || !isPngBlob(blob)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (blob.size > limits.maxItemBytes || blob.size > limits.maxBytes) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return { ...key, blob, width, height, byteSize: blob.size };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function requestResult(request) {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
request.onsuccess = () => resolve(request.result);
|
|
158
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function transactionError(transaction, fallbackMessage) {
|
|
163
|
+
return transaction.error ?? new Error(fallbackMessage);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function openDatabase(factory, dbName) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
let settled = false;
|
|
169
|
+
let request;
|
|
170
|
+
let timer = null;
|
|
171
|
+
|
|
172
|
+
const settle = (callback, value) => {
|
|
173
|
+
if (settled) {
|
|
174
|
+
if (value && typeof value.close === "function") {
|
|
175
|
+
value.close();
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
settled = true;
|
|
180
|
+
if (timer != null) {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
}
|
|
183
|
+
callback(value);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
request = factory.open(dbName, DB_VERSION);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
settle(reject, error);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
request.onupgradeneeded = () => {
|
|
194
|
+
const database = request.result;
|
|
195
|
+
let frameStore;
|
|
196
|
+
if (!database.objectStoreNames.contains(FRAME_STORE)) {
|
|
197
|
+
frameStore = database.createObjectStore(FRAME_STORE, { keyPath: "key" });
|
|
198
|
+
} else {
|
|
199
|
+
frameStore = request.transaction.objectStore(FRAME_STORE);
|
|
200
|
+
}
|
|
201
|
+
if (!frameStore.indexNames.contains(DOCUMENT_INDEX)) {
|
|
202
|
+
frameStore.createIndex(DOCUMENT_INDEX, "documentId", { unique: false });
|
|
203
|
+
}
|
|
204
|
+
if (!frameStore.indexNames.contains(ACCESS_INDEX)) {
|
|
205
|
+
frameStore.createIndex(ACCESS_INDEX, "lastAccess", { unique: false });
|
|
206
|
+
}
|
|
207
|
+
if (!database.objectStoreNames.contains(META_STORE)) {
|
|
208
|
+
database.createObjectStore(META_STORE, { keyPath: "key" });
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
request.onsuccess = () => settle(resolve, request.result);
|
|
212
|
+
request.onerror = () => settle(reject, request.error ?? new Error("Unable to open IndexedDB"));
|
|
213
|
+
request.onblocked = () => settle(reject, new Error("IndexedDB open was blocked"));
|
|
214
|
+
|
|
215
|
+
if (typeof setTimeout === "function") {
|
|
216
|
+
timer = setTimeout(() => {
|
|
217
|
+
settle(reject, new Error("IndexedDB open timed out"));
|
|
218
|
+
}, OPEN_TIMEOUT_MS);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isStoredFrameValid(record) {
|
|
224
|
+
return Boolean(
|
|
225
|
+
record
|
|
226
|
+
&& typeof record === "object"
|
|
227
|
+
&& typeof record.key === "string"
|
|
228
|
+
&& typeof record.documentId === "string"
|
|
229
|
+
&& Number.isSafeInteger(record.pageIndex)
|
|
230
|
+
&& normalizeDimension(record.width) != null
|
|
231
|
+
&& normalizeDimension(record.height) != null
|
|
232
|
+
&& Number.isSafeInteger(record.byteSize)
|
|
233
|
+
&& record.byteSize > 0
|
|
234
|
+
&& isPngBlob(record.blob)
|
|
235
|
+
&& record.blob.size === record.byteSize,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function reconcileUsage(database, limits) {
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
let transaction;
|
|
242
|
+
try {
|
|
243
|
+
transaction = database.transaction([FRAME_STORE, META_STORE], "readwrite");
|
|
244
|
+
} catch (error) {
|
|
245
|
+
reject(error);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const frames = transaction.objectStore(FRAME_STORE);
|
|
250
|
+
const meta = transaction.objectStore(META_STORE);
|
|
251
|
+
let totalBytes = 0;
|
|
252
|
+
let totalEntries = 0;
|
|
253
|
+
|
|
254
|
+
const writeUsage = () => {
|
|
255
|
+
meta.put({
|
|
256
|
+
key: USAGE_KEY,
|
|
257
|
+
totalBytes,
|
|
258
|
+
totalEntries,
|
|
259
|
+
updatedAt: Date.now(),
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const withinLimits = () => (
|
|
264
|
+
totalBytes <= limits.maxBytes
|
|
265
|
+
&& totalEntries <= limits.maxEntries
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const trimOldest = () => {
|
|
269
|
+
if (withinLimits()) {
|
|
270
|
+
writeUsage();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const trimRequest = frames.index(ACCESS_INDEX).openCursor();
|
|
274
|
+
trimRequest.onsuccess = () => {
|
|
275
|
+
const cursor = trimRequest.result;
|
|
276
|
+
if (cursor == null) {
|
|
277
|
+
totalBytes = 0;
|
|
278
|
+
totalEntries = 0;
|
|
279
|
+
writeUsage();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const record = cursor.value;
|
|
283
|
+
cursor.delete();
|
|
284
|
+
if (isStoredFrameValid(record)) {
|
|
285
|
+
totalBytes = Math.max(0, totalBytes - record.byteSize);
|
|
286
|
+
totalEntries = Math.max(0, totalEntries - 1);
|
|
287
|
+
}
|
|
288
|
+
if (withinLimits()) {
|
|
289
|
+
writeUsage();
|
|
290
|
+
} else {
|
|
291
|
+
cursor.continue();
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const cursorRequest = frames.openCursor();
|
|
297
|
+
cursorRequest.onsuccess = () => {
|
|
298
|
+
const cursor = cursorRequest.result;
|
|
299
|
+
if (cursor == null) {
|
|
300
|
+
trimOldest();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const record = cursor.value;
|
|
304
|
+
if (isStoredFrameValid(record) && record.byteSize <= limits.maxItemBytes) {
|
|
305
|
+
totalBytes += record.byteSize;
|
|
306
|
+
totalEntries += 1;
|
|
307
|
+
} else {
|
|
308
|
+
cursor.delete();
|
|
309
|
+
}
|
|
310
|
+
cursor.continue();
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
transaction.oncomplete = () => resolve({ totalBytes, totalEntries });
|
|
314
|
+
transaction.onerror = () => reject(transactionError(transaction, "IndexedDB reconciliation failed"));
|
|
315
|
+
transaction.onabort = () => reject(transactionError(transaction, "IndexedDB reconciliation was aborted"));
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function readUsageRecord(value) {
|
|
320
|
+
const totalBytes = Number.isSafeInteger(value?.totalBytes) && value.totalBytes >= 0
|
|
321
|
+
? value.totalBytes
|
|
322
|
+
: 0;
|
|
323
|
+
const totalEntries = Number.isSafeInteger(value?.totalEntries) && value.totalEntries >= 0
|
|
324
|
+
? value.totalEntries
|
|
325
|
+
: 0;
|
|
326
|
+
return { totalBytes, totalEntries };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function putFrame(database, record, limits) {
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
let transaction;
|
|
332
|
+
try {
|
|
333
|
+
transaction = database.transaction([FRAME_STORE, META_STORE], "readwrite");
|
|
334
|
+
} catch (error) {
|
|
335
|
+
reject(error);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const frames = transaction.objectStore(FRAME_STORE);
|
|
340
|
+
const meta = transaction.objectStore(META_STORE);
|
|
341
|
+
let existing;
|
|
342
|
+
let usage;
|
|
343
|
+
let existingReady = false;
|
|
344
|
+
let usageReady = false;
|
|
345
|
+
let started = false;
|
|
346
|
+
|
|
347
|
+
const failRequest = () => {
|
|
348
|
+
try {
|
|
349
|
+
transaction.abort();
|
|
350
|
+
} catch {
|
|
351
|
+
// The transaction may already have been aborted by the browser.
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const finalizeWrite = () => {
|
|
356
|
+
const now = record.lastAccess;
|
|
357
|
+
frames.put(record);
|
|
358
|
+
meta.put({
|
|
359
|
+
key: USAGE_KEY,
|
|
360
|
+
totalBytes: usage.totalBytes,
|
|
361
|
+
totalEntries: usage.totalEntries,
|
|
362
|
+
updatedAt: now,
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const evictIfNeeded = () => {
|
|
367
|
+
if (started || !existingReady || !usageReady) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
started = true;
|
|
371
|
+
|
|
372
|
+
const oldBytes = isStoredFrameValid(existing) ? existing.byteSize : 0;
|
|
373
|
+
const oldEntries = isStoredFrameValid(existing) ? 1 : 0;
|
|
374
|
+
usage.totalBytes = Math.max(0, usage.totalBytes - oldBytes) + record.byteSize;
|
|
375
|
+
usage.totalEntries = Math.max(0, usage.totalEntries - oldEntries) + 1;
|
|
376
|
+
|
|
377
|
+
const withinLimits = () => (
|
|
378
|
+
usage.totalBytes <= limits.maxBytes
|
|
379
|
+
&& usage.totalEntries <= limits.maxEntries
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
if (withinLimits()) {
|
|
383
|
+
finalizeWrite();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const cursorRequest = frames.index(ACCESS_INDEX).openCursor();
|
|
388
|
+
cursorRequest.onsuccess = () => {
|
|
389
|
+
const cursor = cursorRequest.result;
|
|
390
|
+
if (cursor == null) {
|
|
391
|
+
if (withinLimits()) {
|
|
392
|
+
finalizeWrite();
|
|
393
|
+
} else {
|
|
394
|
+
failRequest();
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const candidate = cursor.value;
|
|
400
|
+
if (candidate?.key !== record.key && isStoredFrameValid(candidate)) {
|
|
401
|
+
cursor.delete();
|
|
402
|
+
usage.totalBytes = Math.max(0, usage.totalBytes - candidate.byteSize);
|
|
403
|
+
usage.totalEntries = Math.max(0, usage.totalEntries - 1);
|
|
404
|
+
} else if (candidate?.key !== record.key) {
|
|
405
|
+
cursor.delete();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (withinLimits()) {
|
|
409
|
+
finalizeWrite();
|
|
410
|
+
} else {
|
|
411
|
+
cursor.continue();
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
cursorRequest.onerror = failRequest;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const existingRequest = frames.get(record.key);
|
|
418
|
+
existingRequest.onsuccess = () => {
|
|
419
|
+
existing = existingRequest.result;
|
|
420
|
+
existingReady = true;
|
|
421
|
+
evictIfNeeded();
|
|
422
|
+
};
|
|
423
|
+
existingRequest.onerror = failRequest;
|
|
424
|
+
|
|
425
|
+
const usageRequest = meta.get(USAGE_KEY);
|
|
426
|
+
usageRequest.onsuccess = () => {
|
|
427
|
+
usage = readUsageRecord(usageRequest.result);
|
|
428
|
+
usageReady = true;
|
|
429
|
+
evictIfNeeded();
|
|
430
|
+
};
|
|
431
|
+
usageRequest.onerror = failRequest;
|
|
432
|
+
|
|
433
|
+
transaction.oncomplete = () => resolve(true);
|
|
434
|
+
transaction.onerror = () => reject(transactionError(transaction, "IndexedDB frame write failed"));
|
|
435
|
+
transaction.onabort = () => reject(transactionError(transaction, "IndexedDB frame write was aborted"));
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function getFrame(database, key, nextAccess) {
|
|
440
|
+
return new Promise((resolve, reject) => {
|
|
441
|
+
let transaction;
|
|
442
|
+
try {
|
|
443
|
+
transaction = database.transaction(FRAME_STORE, "readwrite");
|
|
444
|
+
} catch (error) {
|
|
445
|
+
reject(error);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const store = transaction.objectStore(FRAME_STORE);
|
|
450
|
+
let result = null;
|
|
451
|
+
const request = store.get(key.key);
|
|
452
|
+
request.onsuccess = () => {
|
|
453
|
+
const record = request.result;
|
|
454
|
+
if (!isStoredFrameValid(record)) {
|
|
455
|
+
if (record != null) {
|
|
456
|
+
store.delete(key.key);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
record.lastAccess = nextAccess();
|
|
461
|
+
store.put(record);
|
|
462
|
+
result = {
|
|
463
|
+
blob: record.blob,
|
|
464
|
+
width: record.width,
|
|
465
|
+
height: record.height,
|
|
466
|
+
byteSize: record.byteSize,
|
|
467
|
+
lastAccess: record.lastAccess,
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
transaction.oncomplete = () => resolve(result);
|
|
472
|
+
transaction.onerror = () => reject(transactionError(transaction, "IndexedDB frame read failed"));
|
|
473
|
+
transaction.onabort = () => reject(transactionError(transaction, "IndexedDB frame read was aborted"));
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function deleteFrame(database, key) {
|
|
478
|
+
return new Promise((resolve, reject) => {
|
|
479
|
+
let transaction;
|
|
480
|
+
try {
|
|
481
|
+
transaction = database.transaction([FRAME_STORE, META_STORE], "readwrite");
|
|
482
|
+
} catch (error) {
|
|
483
|
+
reject(error);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const frames = transaction.objectStore(FRAME_STORE);
|
|
488
|
+
const meta = transaction.objectStore(META_STORE);
|
|
489
|
+
let deleted = false;
|
|
490
|
+
const getRequest = frames.get(key.key);
|
|
491
|
+
getRequest.onsuccess = () => {
|
|
492
|
+
const existing = getRequest.result;
|
|
493
|
+
if (existing == null) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
deleted = true;
|
|
497
|
+
const existingIsValid = isStoredFrameValid(existing);
|
|
498
|
+
const usageRequest = meta.get(USAGE_KEY);
|
|
499
|
+
usageRequest.onsuccess = () => {
|
|
500
|
+
const usage = readUsageRecord(usageRequest.result);
|
|
501
|
+
frames.delete(key.key);
|
|
502
|
+
meta.put({
|
|
503
|
+
key: USAGE_KEY,
|
|
504
|
+
totalBytes: Math.max(0, usage.totalBytes - (existingIsValid ? existing.byteSize : 0)),
|
|
505
|
+
totalEntries: Math.max(0, usage.totalEntries - (existingIsValid ? 1 : 0)),
|
|
506
|
+
updatedAt: Date.now(),
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
transaction.oncomplete = () => resolve(deleted);
|
|
512
|
+
transaction.onerror = () => reject(transactionError(transaction, "IndexedDB frame delete failed"));
|
|
513
|
+
transaction.onabort = () => reject(transactionError(transaction, "IndexedDB frame delete was aborted"));
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function clearDocumentFrames(database, documentId) {
|
|
518
|
+
return new Promise((resolve, reject) => {
|
|
519
|
+
let transaction;
|
|
520
|
+
try {
|
|
521
|
+
transaction = database.transaction([FRAME_STORE, META_STORE], "readwrite");
|
|
522
|
+
} catch (error) {
|
|
523
|
+
reject(error);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const frames = transaction.objectStore(FRAME_STORE);
|
|
528
|
+
const meta = transaction.objectStore(META_STORE);
|
|
529
|
+
let usage = null;
|
|
530
|
+
let cursorFinished = false;
|
|
531
|
+
let deletedEntries = 0;
|
|
532
|
+
let deletedBytes = 0;
|
|
533
|
+
let finalized = false;
|
|
534
|
+
|
|
535
|
+
const maybeFinalize = () => {
|
|
536
|
+
if (finalized || usage == null || !cursorFinished) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
finalized = true;
|
|
540
|
+
meta.put({
|
|
541
|
+
key: USAGE_KEY,
|
|
542
|
+
totalBytes: Math.max(0, usage.totalBytes - deletedBytes),
|
|
543
|
+
totalEntries: Math.max(0, usage.totalEntries - deletedEntries),
|
|
544
|
+
updatedAt: Date.now(),
|
|
545
|
+
});
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const usageRequest = meta.get(USAGE_KEY);
|
|
549
|
+
usageRequest.onsuccess = () => {
|
|
550
|
+
usage = readUsageRecord(usageRequest.result);
|
|
551
|
+
maybeFinalize();
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const cursorRequest = frames.index(DOCUMENT_INDEX).openCursor(documentId);
|
|
555
|
+
cursorRequest.onsuccess = () => {
|
|
556
|
+
const cursor = cursorRequest.result;
|
|
557
|
+
if (cursor == null) {
|
|
558
|
+
cursorFinished = true;
|
|
559
|
+
maybeFinalize();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const record = cursor.value;
|
|
563
|
+
if (isStoredFrameValid(record)) {
|
|
564
|
+
deletedEntries += 1;
|
|
565
|
+
deletedBytes += record.byteSize;
|
|
566
|
+
}
|
|
567
|
+
cursor.delete();
|
|
568
|
+
cursor.continue();
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
transaction.oncomplete = () => resolve(deletedEntries);
|
|
572
|
+
transaction.onerror = () => reject(transactionError(transaction, "IndexedDB document clear failed"));
|
|
573
|
+
transaction.onabort = () => reject(transactionError(transaction, "IndexedDB document clear was aborted"));
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Create a cache facade that is safe in Window and DedicatedWorkerGlobalScope.
|
|
579
|
+
* The facade is synchronous; all storage methods are asynchronous and never
|
|
580
|
+
* throw for storage availability, quota, cloning, or transaction failures.
|
|
581
|
+
*/
|
|
582
|
+
function createFrameCache(options = {}) {
|
|
583
|
+
const source = options && typeof options === "object" ? options : {};
|
|
584
|
+
const limits = normalizeFrameCacheLimits(source);
|
|
585
|
+
const dbName = normalizeDatabaseName(source.dbName);
|
|
586
|
+
const factory = source.indexedDB ?? globalThis.indexedDB;
|
|
587
|
+
const enabled = source.enabled !== false;
|
|
588
|
+
let closed = false;
|
|
589
|
+
let disabledReason = !enabled
|
|
590
|
+
? "disabled"
|
|
591
|
+
: factory && typeof factory.open === "function"
|
|
592
|
+
? null
|
|
593
|
+
: "unavailable";
|
|
594
|
+
let databasePromise = null;
|
|
595
|
+
let lastAccess = 0;
|
|
596
|
+
|
|
597
|
+
const nextAccess = () => {
|
|
598
|
+
lastAccess = Math.max(Date.now(), lastAccess + 1);
|
|
599
|
+
return lastAccess;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const disable = (error) => {
|
|
603
|
+
if (disabledReason == null) {
|
|
604
|
+
disabledReason = typeof error?.name === "string" && error.name
|
|
605
|
+
? error.name
|
|
606
|
+
: "storage-error";
|
|
607
|
+
}
|
|
608
|
+
if (databasePromise != null) {
|
|
609
|
+
databasePromise.then((database) => database?.close()).catch(() => {});
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const getDatabase = async () => {
|
|
614
|
+
if (closed || disabledReason != null) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
if (databasePromise == null) {
|
|
618
|
+
databasePromise = openDatabase(factory, dbName)
|
|
619
|
+
.then(async (database) => {
|
|
620
|
+
database.onversionchange = () => {
|
|
621
|
+
database.close();
|
|
622
|
+
disable({ name: "version-change" });
|
|
623
|
+
};
|
|
624
|
+
await reconcileUsage(database, limits);
|
|
625
|
+
return database;
|
|
626
|
+
})
|
|
627
|
+
.catch((error) => {
|
|
628
|
+
disable(error);
|
|
629
|
+
return null;
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
return databasePromise;
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const run = async (operation, fallback) => {
|
|
636
|
+
if (closed || disabledReason != null) {
|
|
637
|
+
return fallback;
|
|
638
|
+
}
|
|
639
|
+
const database = await getDatabase();
|
|
640
|
+
if (database == null) {
|
|
641
|
+
return fallback;
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
return await operation(database);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
// QuotaExceededError, Safari's InvalidStateError / TransactionInactiveError,
|
|
647
|
+
// private-mode failures, and structured-clone failures all become misses.
|
|
648
|
+
disable(error);
|
|
649
|
+
return fallback;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
return Object.freeze({
|
|
654
|
+
async get(keyInput) {
|
|
655
|
+
const key = normalizeFrameCacheKey(keyInput);
|
|
656
|
+
if (key == null) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
return run((database) => getFrame(database, key, nextAccess), null);
|
|
660
|
+
},
|
|
661
|
+
|
|
662
|
+
async put(keyInput, frameInput) {
|
|
663
|
+
const input = normalizePutArguments(keyInput, frameInput, limits);
|
|
664
|
+
if (input == null) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
const record = {
|
|
668
|
+
...input,
|
|
669
|
+
lastAccess: nextAccess(),
|
|
670
|
+
};
|
|
671
|
+
return run((database) => putFrame(database, record, limits), false);
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
async delete(keyInput) {
|
|
675
|
+
const key = normalizeFrameCacheKey(keyInput);
|
|
676
|
+
if (key == null) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
return run((database) => deleteFrame(database, key), false);
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
async clearDocument(documentId) {
|
|
683
|
+
const normalized = normalizeBoundedString(documentId, MAX_DOCUMENT_ID_LENGTH, { trim: true });
|
|
684
|
+
if (normalized == null) {
|
|
685
|
+
return 0;
|
|
686
|
+
}
|
|
687
|
+
return run((database) => clearDocumentFrames(database, normalized), 0);
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
async stats() {
|
|
691
|
+
const unavailable = () => ({
|
|
692
|
+
available: false,
|
|
693
|
+
disabledReason: closed ? "closed" : disabledReason ?? "unavailable",
|
|
694
|
+
entries: 0,
|
|
695
|
+
totalBytes: 0,
|
|
696
|
+
...limits,
|
|
697
|
+
});
|
|
698
|
+
if (closed || disabledReason != null) {
|
|
699
|
+
return unavailable();
|
|
700
|
+
}
|
|
701
|
+
const result = await run(async (database) => {
|
|
702
|
+
let transaction;
|
|
703
|
+
try {
|
|
704
|
+
transaction = database.transaction(META_STORE, "readonly");
|
|
705
|
+
} catch (error) {
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
const value = await requestResult(transaction.objectStore(META_STORE).get(USAGE_KEY));
|
|
709
|
+
const usage = readUsageRecord(value);
|
|
710
|
+
return {
|
|
711
|
+
available: true,
|
|
712
|
+
disabledReason: null,
|
|
713
|
+
entries: usage.totalEntries,
|
|
714
|
+
totalBytes: usage.totalBytes,
|
|
715
|
+
...limits,
|
|
716
|
+
};
|
|
717
|
+
}, null);
|
|
718
|
+
return result ?? unavailable();
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
close() {
|
|
722
|
+
if (closed) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
closed = true;
|
|
726
|
+
if (databasePromise != null) {
|
|
727
|
+
databasePromise.then((database) => database?.close()).catch(() => {});
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
}
|
|
2
732
|
|
|
3
733
|
const WASM_EXPORTS = Object.freeze([
|
|
4
734
|
'memory',
|