Skip to content

Commit 1449795

Browse files
committed
19 - DEMO4 issues sans ImageWorker
1 parent eb7c912 commit 1449795

File tree

6 files changed

+231
-20
lines changed

6 files changed

+231
-20
lines changed

app/app.css

+5
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@
77
font-family: "Pixelify Sans", system-ui, sans-serif;
88
}
99
}
10+
11+
div.data-pokemon-thumbnail-loading {
12+
width: 100%;
13+
aspect-ratio: 1 / 1;
14+
}

app/components/pokemon-details.gts

+29-5
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,43 @@ import Component from '@glimmer/component';
22
import type { Pokemon } from 'ember-polaris-pokedex/schemas/pokemon';
33
import PokemonTypeBadge from 'ember-polaris-pokedex/components/pokemon-type-badge';
44
import PokemonEvolutionNav from 'ember-polaris-pokedex/components/pokemon-evolution-nav';
5+
import { service } from '@ember/service';
6+
import type { ImageFetch } from '@warp-drive/experiments/image-fetch';
7+
import { cached } from '@glimmer/tracking';
8+
import { getPromiseState } from '@warp-drive/ember';
59

610
export default class PokemonDetails extends Component<{
711
Args: { pokemon: Pokemon };
812
}> {
13+
@service declare images: ImageFetch;
14+
15+
@cached
16+
get detailImageRequest() {
17+
return this.images.load(this.args.pokemon.image.hires);
18+
}
19+
20+
@cached
21+
get detailImageUrl() {
22+
const state = getPromiseState(this.detailImage);
23+
if (state.isError || state.isPending) {
24+
return null;
25+
}
26+
return state.result;
27+
}
28+
929
<template>
1030
<div
1131
class='pokemon-details flex flex-col justify-center gap-16 md:flex-row'
1232
>
13-
<img
14-
class='max-size-96 full-embed aspect-square size-96 animate-wiggle drop-shadow-2xl [animation-delay:_0.2s]'
15-
src={{@pokemon.image.hires}}
16-
alt={{@pokemon.name.english}}
17-
/>
33+
{{#if this.detailImageUrl}}
34+
<img
35+
class='max-size-96 full-embed aspect-square size-96 animate-wiggle drop-shadow-2xl [animation-delay:_0.2s]'
36+
src={{this.detailImageUrl}}
37+
alt={{@pokemon.name.english}}
38+
/>
39+
{{else}}
40+
<div class='data-pokemon-thumbnail-loading'></div>
41+
{{/if}}
1842
<div class='max-w-96'>
1943
<h2 class='text-4xl font-medium'>{{@pokemon.name.english}}</h2>
2044
<p class='my-2 text-lg italic text-slate-700'>

app/components/pokemon-evolution-nav.gts

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type RouterService from '@ember/routing/router-service';
55
import { service } from '@ember/service';
66
import { fn } from '@ember/helper';
77
import { on } from '@ember/modifier';
8-
import { preloadImage } from 'ember-polaris-pokedex/components/pokemon-grid-item';
8+
import type { ImageFetch } from '@warp-drive/experiments/image-fetch';
99

1010
// https://raw.githubusercontent.com/IgnaceMaes/pokemon-data.json/master/images/pokedex/hires/005.png
1111
function getHiresImageForId(id: string) {
@@ -19,6 +19,7 @@ export default class PokemonEvolutionNav extends Component<{
1919
Args: { pokemon: Pokemon };
2020
}> {
2121
@service declare router: RouterService;
22+
@service declare images: ImageFetch;
2223

2324
transitionToPokemonDetails = (
2425
pokemonId: string,
@@ -42,7 +43,7 @@ export default class PokemonEvolutionNav extends Component<{
4243

4344
preloadImageForPokemonId = (pokemonId: string) => {
4445
const url = getHiresImageForId(pokemonId);
45-
preloadImage(url);
46+
this.images.load(url);
4647
};
4748

4849
<template>

app/components/pokemon-grid-item.gts

+35-13
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@ import Component from '@glimmer/component';
44
import type { Pokemon } from 'ember-polaris-pokedex/schemas/pokemon';
55
import { service } from '@ember/service';
66
import type RouterService from '@ember/routing/router-service';
7-
8-
export function preloadImage(imageUrl: string) {
9-
const img = new Image();
10-
img.src = imageUrl;
11-
}
7+
import type { ImageFetch } from '@warp-drive/experiments/image-fetch';
8+
import { cached } from '@glimmer/tracking';
9+
import { getPromiseState } from '@warp-drive/ember';
1210

1311
interface PokemonSignature {
1412
Args: { pokemon: Pokemon };
1513
}
1614

1715
export default class PokemonGridItem extends Component<PokemonSignature> {
1816
@service declare router: RouterService;
17+
@service declare images: ImageFetch;
1918

2019
transitionToPokemonDetails = (pokemon: Pokemon, event: MouseEvent) => {
2120
// Fallback for browsers that don't support this API:
@@ -38,20 +37,43 @@ export default class PokemonGridItem extends Component<PokemonSignature> {
3837
});
3938
};
4039

40+
preloadImage = (imageUrl: string) => {
41+
this.images.load(imageUrl);
42+
};
43+
44+
@cached
45+
get thumbnailRequest() {
46+
return this.images.load(this.args.pokemon.image.thumbnail);
47+
}
48+
49+
@cached
50+
get thumbnailUrl() {
51+
return this.args.pokemon.image.thumbnail;
52+
// const state = getPromiseState(this.thumbnailRequest);
53+
// if (state.isError || state.isPending) {
54+
// return null;
55+
// }
56+
// return state.result;
57+
}
58+
4159
<template>
4260
<button
4361
type='button'
4462
class='revealing-image group flex cursor-pointer flex-col items-center rounded-xl bg-gradient-to-br from-pink-100 to-yellow-100 p-4 shadow transition-shadow hover:shadow-md'
45-
{{on 'pointerenter' (fn preloadImage @pokemon.image.hires)}}
63+
{{on 'pointerenter' (fn this.preloadImage @pokemon.image.hires)}}
4664
{{on 'click' (fn this.transitionToPokemonDetails @pokemon)}}
4765
>
48-
<img
49-
data-pokemon-thumbnail
50-
class='block aspect-square w-full p-4 transition-transform group-hover:scale-125 group-hover:drop-shadow-xl'
51-
loading='lazy'
52-
src={{@pokemon.image.thumbnail}}
53-
alt='{{@pokemon.name.english}} thumbnail'
54-
/>
66+
{{#if this.thumbnailUrl}}
67+
<img
68+
data-pokemon-thumbnail
69+
class='block aspect-square w-full p-4 transition-transform group-hover:scale-125 group-hover:drop-shadow-xl'
70+
loading='lazy'
71+
src={{this.thumbnailUrl}}
72+
alt='{{@pokemon.name.english}} thumbnail'
73+
/>
74+
{{else}}
75+
<div class='data-pokemon-thumbnail-loading'></div>
76+
{{/if}}
5577
<span class='mt-4 text-lg font-medium'>{{@pokemon.name.english}}</span>
5678
</button>
5779
</template>

app/data-worker/image-worker.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ImageWorker } from '@warp-drive/experiments/image-worker';
2+
3+
new ImageWorker();

app/services/images.ts

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { createDeferred } from '@ember-data/request';
2+
import type { Deferred } from '@ember-data/request/-private/types';
3+
4+
export type SuccessResponseEventData = {
5+
type: 'success-response';
6+
thread: string;
7+
url: string;
8+
};
9+
export type ErrorResponseEventData = {
10+
type: 'error-response';
11+
thread: string;
12+
url: string;
13+
};
14+
15+
export type RequestEventData = {
16+
type: 'load';
17+
thread: string;
18+
url: string;
19+
};
20+
21+
export type ThreadInitEventData = {
22+
type: 'connect';
23+
thread: string;
24+
};
25+
26+
export type MainThreadEvent = MessageEvent<
27+
SuccessResponseEventData | ErrorResponseEventData
28+
>;
29+
export type WorkerThreadEvent =
30+
| MessageEvent<RequestEventData>
31+
| MessageEvent<ThreadInitEventData>;
32+
33+
import { assert } from '@ember/debug';
34+
35+
export interface FastBoot {
36+
// eslint-disable-next-line no-unused-vars
37+
require(_moduleName: string): unknown;
38+
isFastBoot: boolean;
39+
request: Request;
40+
}
41+
42+
const isServerEnv = typeof FastBoot !== 'undefined';
43+
44+
// This is a copy of WarpDrive's ImageFetch class with
45+
// a temporary fix for an issue with which url is returned.
46+
export class ImageFetch {
47+
declare worker: Worker | SharedWorker;
48+
declare threadId: string;
49+
declare pending: Map<string, Deferred<string>>;
50+
declare channel: MessageChannel;
51+
declare cache: Map<string, string>;
52+
declare queue: MainThreadEvent[];
53+
54+
constructor(worker: Worker | SharedWorker | null) {
55+
this.threadId = isServerEnv ? '' : crypto.randomUUID();
56+
this.pending = new Map();
57+
this.cache = new Map();
58+
this.queue = [];
59+
60+
const isTesting = false;
61+
assert(
62+
`Expected a SharedWorker instance`,
63+
isTesting || isServerEnv || worker instanceof SharedWorker,
64+
);
65+
this.worker = worker as SharedWorker;
66+
67+
if (!isServerEnv) {
68+
const fn = (event: MainThreadEvent) => {
69+
this.queue.push(event);
70+
if (this.queue.length === 1) {
71+
setTimeout(() => {
72+
this.processQueue();
73+
}, 0);
74+
}
75+
};
76+
77+
if (worker instanceof SharedWorker) {
78+
worker.port.postMessage({ type: 'connect', thread: this.threadId });
79+
worker.port.onmessage = fn;
80+
} else if (worker) {
81+
this.channel = new MessageChannel();
82+
worker.postMessage({ type: 'connect', thread: this.threadId }, [
83+
this.channel.port2,
84+
]);
85+
86+
this.channel.port1.onmessage = fn;
87+
}
88+
}
89+
}
90+
91+
processQueue() {
92+
const queue = this.queue;
93+
this.queue = [];
94+
95+
queue.forEach((event) => {
96+
const { type, url, objectUrl } = event.data;
97+
const deferred = this.cleanupRequest(url);
98+
if (!deferred) {
99+
return;
100+
}
101+
102+
if (type === 'success-response') {
103+
deferred.resolve(objectUrl);
104+
return;
105+
}
106+
107+
if (type === 'error-response') {
108+
deferred.reject(null);
109+
return;
110+
}
111+
});
112+
}
113+
114+
cleanupRequest(url: string) {
115+
const deferred = this.pending.get(url);
116+
this.pending.delete(url);
117+
118+
return deferred;
119+
}
120+
121+
_send(event: RequestEventData) {
122+
this.worker instanceof SharedWorker
123+
? this.worker.port.postMessage(event)
124+
: this.channel.port1.postMessage(event);
125+
}
126+
127+
load(url: string) {
128+
if (isServerEnv) {
129+
return Promise.resolve(url);
130+
}
131+
132+
const objectUrl = this.cache.get(url);
133+
if (objectUrl) {
134+
return Promise.resolve(objectUrl);
135+
}
136+
137+
const deferred = createDeferred<string>();
138+
this.pending.set(url, deferred);
139+
this._send({ type: 'load', thread: this.threadId, url });
140+
return deferred.promise;
141+
}
142+
}
143+
144+
export default {
145+
create() {
146+
return new ImageFetch(
147+
new SharedWorker(
148+
new URL('../data-worker/image-worker.ts', import.meta.url),
149+
{
150+
name: 'ImageWorker',
151+
type: 'module',
152+
},
153+
),
154+
);
155+
},
156+
};

0 commit comments

Comments
 (0)