@octane-xplat/gif 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -9
- package/src/AnimatedImage.windows.tsrx +8 -0
- package/src/index.windows.ts +5 -0
- package/src/prefetch-batch.ts +7 -2
- package/src/prefetch.windows.ts +8 -0
- package/types/generated/.tsrx-typegen-manifest.json +1 -0
- package/types/generated/native-elements.d.ts +8 -0
- package/types/generated/prefetch-batch.d.ts +1 -1
- package/types/index.macos.d.ts +4 -1
- package/dist/native/AnimatedImage.tsrx.js +0 -39
- package/dist/native/escape-props.js +0 -12
- package/dist/native/index.js +0 -3
- package/dist/native/octane-client-build.json +0 -8
- package/dist/native/prefetch-batch.js +0 -27
- package/dist/native/prefetch.js +0 -26
- package/dist/web/AnimatedImage.web.tsrx.js +0 -48
- package/dist/web/escape-props.web.js +0 -18
- package/dist/web/index.js +0 -3
- package/dist/web/octane-client-build.json +0 -8
- package/dist/web/prefetch-batch.js +0 -27
- package/dist/web/prefetch.web.js +0 -17
- package/src/prefetch.web.test.ts +0 -104
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/gif",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Animated images (GIF/animated webp) for Octane xplat — web <img> + @nativescript-community/ui-image",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"android",
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
"directory": "packages/gif"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
-
"dist",
|
|
27
26
|
"types",
|
|
28
|
-
"src"
|
|
27
|
+
"src",
|
|
28
|
+
"!src/**/*.test.*",
|
|
29
|
+
"!src/**/*.spec.*"
|
|
29
30
|
],
|
|
30
31
|
"type": "module",
|
|
31
32
|
"exports": {
|
|
@@ -36,14 +37,14 @@
|
|
|
36
37
|
},
|
|
37
38
|
"web": {
|
|
38
39
|
"types": "./types/generated/index.web.d.ts",
|
|
39
|
-
"default": "./
|
|
40
|
+
"default": "./src/index.web.ts"
|
|
40
41
|
},
|
|
41
42
|
"native": {
|
|
42
43
|
"types": "./types/generated/index.d.ts",
|
|
43
|
-
"default": "./
|
|
44
|
+
"default": "./src/index.ts"
|
|
44
45
|
},
|
|
45
46
|
"types": "./types/generated/index.d.ts",
|
|
46
|
-
"default": "./
|
|
47
|
+
"default": "./src/index.ts"
|
|
47
48
|
}
|
|
48
49
|
},
|
|
49
50
|
"publishConfig": {
|
|
@@ -53,12 +54,13 @@
|
|
|
53
54
|
"@nativescript-community/ui-image": "4.6.20"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
|
-
"@nativescript-community/octane": "0.2.
|
|
57
|
+
"@nativescript-community/octane": "0.2.5",
|
|
57
58
|
"@nativescript/core": "9.1.3",
|
|
59
|
+
"@octane-xplat/macos-renderer": "0.16.0",
|
|
58
60
|
"@octanejs/vite-plugin": "0.1.61",
|
|
59
61
|
"@tsrx/typescript-plugin": "0.4.11",
|
|
60
62
|
"octane": "0.11.0",
|
|
61
|
-
"tsrx-typegen": "0.
|
|
63
|
+
"tsrx-typegen": "0.3.1",
|
|
62
64
|
"typescript": "5.9.3",
|
|
63
65
|
"vite": "8.3.0"
|
|
64
66
|
},
|
|
@@ -76,7 +78,6 @@
|
|
|
76
78
|
}
|
|
77
79
|
},
|
|
78
80
|
"scripts": {
|
|
79
|
-
"build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
|
|
80
81
|
"test:packed": "node tests/packed-consumer.mjs",
|
|
81
82
|
"typegen": "tsrx-typegen --target octane",
|
|
82
83
|
"pack:check": "tsrx-typegen --pack-check"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import type { AnimatedImageProps } from './props'
|
|
3
|
+
|
|
4
|
+
/** Unsupported placeholder — the Windows host has no animated-image engine
|
|
5
|
+
* (ui-image ships iOS/Android only). */
|
|
6
|
+
export function AnimatedImage(_props: AnimatedImageProps) @{
|
|
7
|
+
<label className="vx-unsupported" text="Animated images are unsupported on the Windows host" />
|
|
8
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Windows barrel — explicit leaf imports keep @nativescript-community/
|
|
2
|
+
// ui-image (iOS/Android only) out of the windows graph at the file boundary.
|
|
3
|
+
export { AnimatedImage } from './AnimatedImage.windows.tsrx'
|
|
4
|
+
export { prefetch } from './prefetch.windows'
|
|
5
|
+
export type { AnimatedImageProps, PrefetchOptions } from './props'
|
package/src/prefetch-batch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** VENDORED — identical copy in packages/
|
|
1
|
+
/** VENDORED — identical copy in packages/image/src/prefetch-batch.ts; keep in
|
|
2
2
|
* sync.
|
|
3
3
|
*
|
|
4
4
|
* Shared batch driver for the image-engine leaves' `prefetch` contract:
|
|
@@ -26,16 +26,21 @@ export function prefetchBatch(
|
|
|
26
26
|
return new Promise<boolean>((resolve) => {
|
|
27
27
|
let next = 0
|
|
28
28
|
let active = Math.min(limit, urls.length)
|
|
29
|
+
// Surviving workers stop dequeuing once a failure decides the result —
|
|
30
|
+
// only already-running warm() calls settle on their own.
|
|
31
|
+
let failed = false
|
|
29
32
|
for (let i = 0; i < active; i++) {
|
|
30
33
|
void (async () => {
|
|
31
|
-
while (next < urls.length) {
|
|
34
|
+
while (!failed && next < urls.length) {
|
|
32
35
|
const url = urls[next++]
|
|
33
36
|
try {
|
|
34
37
|
if (!(await warm(url))) {
|
|
38
|
+
failed = true
|
|
35
39
|
resolve(false)
|
|
36
40
|
return
|
|
37
41
|
}
|
|
38
42
|
} catch {
|
|
43
|
+
failed = true
|
|
39
44
|
resolve(false)
|
|
40
45
|
return
|
|
41
46
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PrefetchOptions } from './props'
|
|
2
|
+
|
|
3
|
+
/** prefetch — no-op on Windows: there is no image pipeline to warm
|
|
4
|
+
* (AnimatedImage renders a placeholder there). Resolves false to report
|
|
5
|
+
* that honestly. */
|
|
6
|
+
export function prefetch(_srcs: string | string[], _options?: PrefetchOptions): Promise<boolean> {
|
|
7
|
+
return Promise.resolve(false)
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Img } from '@nativescript-community/ui-image'
|
|
2
|
+
import type { Attributes } from '@nativescript-community/octane/intrinsics'
|
|
3
|
+
|
|
4
|
+
declare module '@nativescript-community/octane/intrinsics' {
|
|
5
|
+
interface NativeScriptElements {
|
|
6
|
+
animatedimage: Attributes<typeof Img>
|
|
7
|
+
}
|
|
8
|
+
}
|
package/types/index.macos.d.ts
CHANGED
|
@@ -2,4 +2,7 @@ import type { AnimatedImageProps, PrefetchOptions } from './generated/props.js'
|
|
|
2
2
|
|
|
3
3
|
export type { AnimatedImageProps, PrefetchOptions } from './generated/props.js'
|
|
4
4
|
export declare function AnimatedImage(props: AnimatedImageProps): unknown
|
|
5
|
-
export declare function prefetch(
|
|
5
|
+
export declare function prefetch(
|
|
6
|
+
srcs: string | string[],
|
|
7
|
+
options?: PrefetchOptions,
|
|
8
|
+
): Promise<boolean>
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { applyEscapeProps } from "./escape-props.js";
|
|
2
|
-
import { defineUniversalComponent, registerElement, universalPlan, universalValue } from "@nativescript-community/octane";
|
|
3
|
-
import { Img } from "@nativescript-community/ui-image";
|
|
4
|
-
//#region src/AnimatedImage.tsrx
|
|
5
|
-
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
6
|
-
"kind": "host",
|
|
7
|
-
"type": "animatedimage",
|
|
8
|
-
"props": {
|
|
9
|
-
"animatedImageView": true,
|
|
10
|
-
"autoPlayAnimations": true
|
|
11
|
-
},
|
|
12
|
-
"bindings": [
|
|
13
|
-
["ref", 0],
|
|
14
|
-
["id", 1],
|
|
15
|
-
["className", 2],
|
|
16
|
-
["style", 3],
|
|
17
|
-
["src", 4],
|
|
18
|
-
["stretch", 5],
|
|
19
|
-
["width", 6],
|
|
20
|
-
["height", 7],
|
|
21
|
-
["accessibilityLabel", 8]
|
|
22
|
-
]
|
|
23
|
-
});
|
|
24
|
-
registerElement("animatedimage", Img);
|
|
25
|
-
var AnimatedImage = defineUniversalComponent("nativescript", function AnimatedImage(props) {
|
|
26
|
-
return universalValue(__octaneUniversalPlan0, [
|
|
27
|
-
(v) => applyEscapeProps(v, props),
|
|
28
|
-
props.id,
|
|
29
|
-
props.className,
|
|
30
|
-
props.style,
|
|
31
|
-
props.src,
|
|
32
|
-
props.stretch ?? "aspectFit",
|
|
33
|
-
props.width,
|
|
34
|
-
props.height,
|
|
35
|
-
props.alt
|
|
36
|
-
]);
|
|
37
|
-
}, { "module": "@nativescript-community/octane" });
|
|
38
|
-
//#endregion
|
|
39
|
-
export { AnimatedImage };
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { isAndroid, isIOS } from "@nativescript/core";
|
|
2
|
-
//#region src/escape-props.ts
|
|
3
|
-
/** Apply only the active device's escape bag after the shared props — the
|
|
4
|
-
* same convention @octane-xplat/ui uses, kept local so this leaf does not
|
|
5
|
-
* depend on ui. */
|
|
6
|
-
function applyEscapeProps(view, props) {
|
|
7
|
-
if (!view) return;
|
|
8
|
-
const bag = isIOS ? props.ios : isAndroid ? props.android : void 0;
|
|
9
|
-
if (bag) Object.assign(view, bag);
|
|
10
|
-
}
|
|
11
|
-
//#endregion
|
|
12
|
-
export { applyEscapeProps };
|
package/dist/native/index.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
function prefetchBatch(srcs, concurrency, warm) {
|
|
2
|
-
const urls = Array.isArray(srcs) ? srcs : [srcs];
|
|
3
|
-
if (urls.length === 0) return Promise.resolve(true);
|
|
4
|
-
const requested = Math.floor(concurrency ?? 5);
|
|
5
|
-
const limit = Number.isFinite(requested) && requested > 0 ? requested : 5;
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
let next = 0;
|
|
8
|
-
let active = Math.min(limit, urls.length);
|
|
9
|
-
for (let i = 0; i < active; i++) (async () => {
|
|
10
|
-
while (next < urls.length) {
|
|
11
|
-
const url = urls[next++];
|
|
12
|
-
try {
|
|
13
|
-
if (!await warm(url)) {
|
|
14
|
-
resolve(false);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
} catch {
|
|
18
|
-
resolve(false);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
if (--active === 0) resolve(true);
|
|
23
|
-
})();
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
//#endregion
|
|
27
|
-
export { prefetchBatch };
|
package/dist/native/prefetch.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { prefetchBatch } from "./prefetch-batch.js";
|
|
2
|
-
import { getImagePipeline, initialize } from "@nativescript-community/ui-image";
|
|
3
|
-
import { isAndroid } from "@nativescript/core";
|
|
4
|
-
//#region src/prefetch.ts
|
|
5
|
-
/** prefetch — warm the image engine's disk cache for `srcs` without displaying
|
|
6
|
-
* anything. Android (Fresco): `prefetchToDiskCache` stores encoded bytes on
|
|
7
|
-
* disk and defers bitmap decode to display. iOS (SDWebImage): the prefetcher
|
|
8
|
-
* stores the downloaded image to the disk cache only. Either way a later
|
|
9
|
-
* `AnimatedImage` with the same src hits disk instead of the network.
|
|
10
|
-
* Resolves false when any URL fails; resolves false when the pipeline is not
|
|
11
|
-
* available yet (e.g. called before app launch).
|
|
12
|
-
* `options.concurrency` caps parallel fetches (default 5); `options.headers`
|
|
13
|
-
* is honored on Android and silently dropped on iOS (upstream gap — see
|
|
14
|
-
* PrefetchOptions). */
|
|
15
|
-
function prefetch(srcs, options) {
|
|
16
|
-
try {
|
|
17
|
-
if (isAndroid) initialize();
|
|
18
|
-
const pipeline = getImagePipeline();
|
|
19
|
-
const { concurrency, ...engineOptions } = options ?? {};
|
|
20
|
-
return prefetchBatch(srcs, concurrency, (url) => pipeline.prefetchToDiskCache(url, engineOptions).then(() => true, () => false));
|
|
21
|
-
} catch {
|
|
22
|
-
return Promise.resolve(false);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
//#endregion
|
|
26
|
-
export { prefetch };
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { applyEscapeProps } from "./escape-props.web.js";
|
|
2
|
-
import { __s, bag7, clone, queueRefAttach, queueRefDetach, setStyle, template } from "octane";
|
|
3
|
-
import { replaceRef, setClassNameIfChanged, setPlainAttributeIfChanged, setURLAttributeIfChanged } from "octane/internal/client";
|
|
4
|
-
//#region src/AnimatedImage.web.tsrx
|
|
5
|
-
var _t$0 = /* @__PURE__ */ template("<img/>");
|
|
6
|
-
var _rm$0 = [
|
|
7
|
-
"r",
|
|
8
|
-
"b",
|
|
9
|
-
"a"
|
|
10
|
-
];
|
|
11
|
-
var OBJECT_FIT = {
|
|
12
|
-
none: "none",
|
|
13
|
-
fill: "fill",
|
|
14
|
-
aspectFit: "contain",
|
|
15
|
-
aspectFill: "cover"
|
|
16
|
-
};
|
|
17
|
-
var AnimatedImage = /* @__PURE__ */ __s(function AnimatedImage(props, __s, __extra) {
|
|
18
|
-
let _b = __s.slots[0];
|
|
19
|
-
if (_b === void 0) {
|
|
20
|
-
let _m1;
|
|
21
|
-
const _root = clone(_t$0);
|
|
22
|
-
queueRefAttach(__s, _m1 = (e) => applyEscapeProps(e, props), _root);
|
|
23
|
-
(__s.cleanups ??= []).push(() => _b && queueRefDetach(_b.b, _b.a));
|
|
24
|
-
_b = bag7(__s, _root, _root, _m1);
|
|
25
|
-
__s.refFields = _rm$0;
|
|
26
|
-
} else {
|
|
27
|
-
const _r = (e) => applyEscapeProps(e, props);
|
|
28
|
-
if (_r !== _b.b) _b.b = replaceRef(__s, _b.b, _r, _b.a);
|
|
29
|
-
}
|
|
30
|
-
_b.c = setPlainAttributeIfChanged(props.id, _b.c, _b.a, "id");
|
|
31
|
-
_b.d = setClassNameIfChanged(props.className, _b.d, _b.a);
|
|
32
|
-
{
|
|
33
|
-
const _v = {
|
|
34
|
-
objectFit: OBJECT_FIT[props.stretch ?? "aspectFit"],
|
|
35
|
-
width: props.width,
|
|
36
|
-
height: props.height,
|
|
37
|
-
...props.style
|
|
38
|
-
};
|
|
39
|
-
if (_b.e !== _v) {
|
|
40
|
-
setStyle(_b.a, _v, _b.e);
|
|
41
|
-
_b.e = _v;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
_b.f = setURLAttributeIfChanged(props.src, _b.f, _b.a, "src");
|
|
45
|
-
_b.g = setPlainAttributeIfChanged(props.alt, _b.g, _b.a, "alt");
|
|
46
|
-
});
|
|
47
|
-
//#endregion
|
|
48
|
-
export { AnimatedImage };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
//#region src/escape-props.web.ts
|
|
2
|
-
/** Apply DOM escape props after the primitive's own props. Dash-named keys
|
|
3
|
-
* are attributes; other keys use the DOM property where available. Mirrors
|
|
4
|
-
* @octane-xplat/ui's web escape-props so this leaf does not depend on ui. */
|
|
5
|
-
function applyEscapeProps(element, props) {
|
|
6
|
-
if (!element || !props.web) return;
|
|
7
|
-
for (const [key, value] of Object.entries(props.web)) if (key.includes("-")) {
|
|
8
|
-
if (value == null || value === false) element.removeAttribute(key);
|
|
9
|
-
else element.setAttribute(key, value === true ? "" : String(value));
|
|
10
|
-
} else if (key in element) try {
|
|
11
|
-
element[key] = value;
|
|
12
|
-
} catch {
|
|
13
|
-
element.setAttribute(key, String(value));
|
|
14
|
-
}
|
|
15
|
-
else if (value != null) element.setAttribute(key, String(value));
|
|
16
|
-
}
|
|
17
|
-
//#endregion
|
|
18
|
-
export { applyEscapeProps };
|
package/dist/web/index.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
function prefetchBatch(srcs, concurrency, warm) {
|
|
2
|
-
const urls = Array.isArray(srcs) ? srcs : [srcs];
|
|
3
|
-
if (urls.length === 0) return Promise.resolve(true);
|
|
4
|
-
const requested = Math.floor(concurrency ?? 5);
|
|
5
|
-
const limit = Number.isFinite(requested) && requested > 0 ? requested : 5;
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
let next = 0;
|
|
8
|
-
let active = Math.min(limit, urls.length);
|
|
9
|
-
for (let i = 0; i < active; i++) (async () => {
|
|
10
|
-
while (next < urls.length) {
|
|
11
|
-
const url = urls[next++];
|
|
12
|
-
try {
|
|
13
|
-
if (!await warm(url)) {
|
|
14
|
-
resolve(false);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
} catch {
|
|
18
|
-
resolve(false);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
if (--active === 0) resolve(true);
|
|
23
|
-
})();
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
//#endregion
|
|
27
|
-
export { prefetchBatch };
|
package/dist/web/prefetch.web.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { prefetchBatch } from "./prefetch-batch.js";
|
|
2
|
-
//#region src/prefetch.web.ts
|
|
3
|
-
/** prefetch — load each `src` through a throwaway <img> so the response lands
|
|
4
|
-
* in the browser HTTP cache; decode and render stay with the displaying
|
|
5
|
-
* element. `options.headers` is ignored — an <img> request cannot carry
|
|
6
|
-
* custom headers. `options.concurrency` caps parallel loads (default 5).
|
|
7
|
-
* Resolves false when any URL fails. */
|
|
8
|
-
function prefetch(srcs, options) {
|
|
9
|
-
return prefetchBatch(srcs, options?.concurrency, (url) => new Promise((resolve) => {
|
|
10
|
-
const image = new Image();
|
|
11
|
-
image.onload = () => resolve(true);
|
|
12
|
-
image.onerror = () => resolve(false);
|
|
13
|
-
image.src = url;
|
|
14
|
-
}));
|
|
15
|
-
}
|
|
16
|
-
//#endregion
|
|
17
|
-
export { prefetch };
|
package/src/prefetch.web.test.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { prefetch } from './prefetch.web'
|
|
3
|
-
import { prefetchBatch } from './prefetch-batch'
|
|
4
|
-
|
|
5
|
-
// Same web prefetch contract as @octane-xplat/image: throwaway <img> per URL,
|
|
6
|
-
// awaited until load/error, false when any URL fails, JS-side concurrency cap.
|
|
7
|
-
|
|
8
|
-
class MockImage {
|
|
9
|
-
static instances: MockImage[] = []
|
|
10
|
-
onload: (() => void) | null = null
|
|
11
|
-
onerror: (() => void) | null = null
|
|
12
|
-
set src(_url: string) {
|
|
13
|
-
MockImage.instances.push(this)
|
|
14
|
-
}
|
|
15
|
-
load() {
|
|
16
|
-
this.onload?.()
|
|
17
|
-
}
|
|
18
|
-
fail() {
|
|
19
|
-
this.onerror?.()
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
24
|
-
|
|
25
|
-
async function drainPending(promise: Promise<boolean>) {
|
|
26
|
-
let result: boolean | undefined
|
|
27
|
-
void promise.then((value) => {
|
|
28
|
-
result = value
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
while (result === undefined) {
|
|
32
|
-
for (const image of MockImage.instances.splice(0)) {
|
|
33
|
-
image.load()
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
await flush()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return result
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe('prefetch (web)', () => {
|
|
43
|
-
afterEach(() => {
|
|
44
|
-
MockImage.instances = []
|
|
45
|
-
vi.unstubAllGlobals()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('resolves true only after every <img> fires load', async () => {
|
|
49
|
-
vi.stubGlobal('Image', MockImage)
|
|
50
|
-
const promise = prefetch(['https://x/a.gif', 'https://x/b.gif'])
|
|
51
|
-
let settled = false
|
|
52
|
-
void promise.then(() => {
|
|
53
|
-
settled = true
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
await flush()
|
|
57
|
-
expect(settled).toBe(false)
|
|
58
|
-
expect(await drainPending(promise)).toBe(true)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('resolves false when any URL fails', async () => {
|
|
62
|
-
vi.stubGlobal('Image', MockImage)
|
|
63
|
-
const promise = prefetch(['https://x/a.gif', 'https://x/b.gif'])
|
|
64
|
-
const [failed] = MockImage.instances.splice(0)
|
|
65
|
-
failed.fail()
|
|
66
|
-
expect(await promise).toBe(false)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('accepts a single URL and resolves true for an empty batch', async () => {
|
|
70
|
-
vi.stubGlobal('Image', MockImage)
|
|
71
|
-
expect(await prefetch([])).toBe(true)
|
|
72
|
-
const promise = prefetch('https://x/a.gif')
|
|
73
|
-
expect(MockImage.instances).toHaveLength(1)
|
|
74
|
-
expect(await drainPending(promise)).toBe(true)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('caps in-flight loads at options.concurrency', async () => {
|
|
78
|
-
vi.stubGlobal('Image', MockImage)
|
|
79
|
-
const promise = prefetch(
|
|
80
|
-
Array.from({ length: 7 }, (_, i) => `https://x/${i}.gif`),
|
|
81
|
-
{ concurrency: 2, headers: { Authorization: 'Bearer x' } },
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
expect(MockImage.instances).toHaveLength(2)
|
|
85
|
-
MockImage.instances.splice(0).forEach((image) => image.load())
|
|
86
|
-
await flush()
|
|
87
|
-
await flush()
|
|
88
|
-
expect(MockImage.instances.length).toBeLessThanOrEqual(2)
|
|
89
|
-
expect(await drainPending(promise)).toBe(true)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('defaults to five parallel loads', async () => {
|
|
93
|
-
vi.stubGlobal('Image', MockImage)
|
|
94
|
-
const promise = prefetch(Array.from({ length: 9 }, (_, i) => `https://x/${i}.gif`))
|
|
95
|
-
expect(MockImage.instances).toHaveLength(5)
|
|
96
|
-
expect(await drainPending(promise)).toBe(true)
|
|
97
|
-
})
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
describe('prefetchBatch', () => {
|
|
101
|
-
it('reports false if the warm callback throws', async () => {
|
|
102
|
-
expect(await prefetchBatch(['a', 'b'], 5, () => Promise.reject(new Error('boom')))).toBe(false)
|
|
103
|
-
})
|
|
104
|
-
})
|